From 9366afd43023f45345b34b7dde05c53d71dfd30f Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Mon, 8 Jun 2026 12:32:15 +0930 Subject: [PATCH 01/72] btrfs: remove btrfs_dio_data::submitted This member records how many bytes are submitted for a direct read/write, utilized by iomap_end() callback to handle short IO cases. However iomap_end() callback is already providing an internally tracked @written member, which is doing the same accounting and providing the same value as btrfs_dio_data::submitted. There is no need to duplicate the work, just remove btrfs_dio_data::submitted. Reviewed-by: Filipe Manana Signed-off-by: Qu Wenruo Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/direct-io.c | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/fs/btrfs/direct-io.c b/fs/btrfs/direct-io.c index 460326d34143..b2add124cc89 100644 --- a/fs/btrfs/direct-io.c +++ b/fs/btrfs/direct-io.c @@ -14,7 +14,6 @@ #include "ordered-data.h" struct btrfs_dio_data { - ssize_t submitted; loff_t old_isize; struct extent_changeset *data_reserved; struct btrfs_ordered_extent *ordered; @@ -619,7 +618,6 @@ static int btrfs_dio_iomap_end(struct inode *inode, loff_t pos, loff_t length, { struct iomap_iter *iter = container_of(iomap, struct iomap_iter, iomap); struct btrfs_dio_data *dio_data = iter->private; - size_t submitted = dio_data->submitted; const bool write = !!(flags & IOMAP_WRITE); int ret = 0; @@ -630,9 +628,9 @@ static int btrfs_dio_iomap_end(struct inode *inode, loff_t pos, loff_t length, return 0; } - if (submitted < length) { - pos += submitted; - length -= submitted; + if (written < length) { + pos += written; + length -= written; if (write) { /* * Got a short write and have updated the isize, need to @@ -659,7 +657,7 @@ static int btrfs_dio_iomap_end(struct inode *inode, loff_t pos, loff_t length, if (dio_data->updated_isize) { u64 new_isize; - if (submitted == 0) + if (written == 0) new_isize = dio_data->old_isize; else new_isize = max(dio_data->old_isize, pos); @@ -772,8 +770,6 @@ static void btrfs_dio_submit_io(const struct iomap_iter *iter, struct bio *bio, dip->file_offset = file_offset; dip->bytes = bio->bi_iter.bi_size; - dio_data->submitted += bio->bi_iter.bi_size; - /* * Check if we are doing a partial write. If we are, we need to split * the ordered extent to match the submitted bio. Hang on to the From 4ac48dc0cbd65fc91c45de17c3afadab85f4bb79 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Mon, 8 Jun 2026 12:32:16 +0930 Subject: [PATCH 02/72] btrfs: refactor btrfs_dio_iomap_end() That function has the following problems: - Read/write handling scattered across different locations E.g. At the beginning there is a dedicated hole read handling, but later short read handling is at an if() branch. - Modifying of @pos and @length parameter for short read Although it's completely fine to modify those parameters as they are passed by value, but it can still be confusing to read. As normally we would assume @pos and @length to be the original range. But for short IO handling we modify @pos/@length, and completely ignore @written. - Unnecessary split for ordered extent and changeset handling Both OE and changeset are only for writes, but they are handled in two different if (write) {} blocks. Refactor the function so that: - Handling of reads and writes are concentrated in their code block Now the handling of reads are in its own small if () branch. Leaving the more complex writes handling to take the remaining function, and reduce the indent level. This also removes all unnecessary "if (write)" checks. - Do not modify @pos and @length Let short IO handling to manually calculate the remaining range. Reviewed-by: Filipe Manana Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/direct-io.c | 124 ++++++++++++++++++++++--------------------- 1 file changed, 64 insertions(+), 60 deletions(-) diff --git a/fs/btrfs/direct-io.c b/fs/btrfs/direct-io.c index b2add124cc89..3e227292b0ac 100644 --- a/fs/btrfs/direct-io.c +++ b/fs/btrfs/direct-io.c @@ -621,74 +621,78 @@ static int btrfs_dio_iomap_end(struct inode *inode, loff_t pos, loff_t length, const bool write = !!(flags & IOMAP_WRITE); int ret = 0; - if (!write && (iomap->type == IOMAP_HOLE)) { - /* If reading from a hole, unlock and return */ - btrfs_unlock_dio_extent(&BTRFS_I(inode)->io_tree, pos, - pos + length - 1, NULL); + if (!write) { + /* + * Hole read, nothing is submitted, thus we have to unlock + * the whole range. + */ + if (iomap->type == IOMAP_HOLE) { + btrfs_unlock_dio_extent(&BTRFS_I(inode)->io_tree, pos, + pos + length - 1, NULL); + return 0; + } + /* + * Short read, needs to unlock the remaining range, and + * return -ENOTBLK so we can later fault in the pages and retry. + */ + if (written < length) { + btrfs_unlock_dio_extent(&BTRFS_I(inode)->io_tree, pos + written, + pos + length - 1, NULL); + return -ENOTBLK; + } + /* The full range is submitted, endio will do the unlock. */ return 0; } if (written < length) { - pos += written; - length -= written; - if (write) { - /* - * Got a short write and have updated the isize, need to - * revert the isize change. - * - * Normally we need to update isize with extent lock hold, - * but we're safe due to the following factors: - * - * - Only a single writer can be enlarging isize - * Enlarging isize will take the exclusive inode lock. - * - * - Buffered readers need to wait for the OE we're holding - * Buffered readers will lock extent and wait for OE - * of the folio range, and since page cache is invalidated - * the OE wait can not be skipped. - * - * So here we are safe to revert the isize before - * finishing the OE, and no reader of the remaining range - * can see the enlarged size. - * - * TODO: Extend the DIO_LOCKED lifespan for direct writes, - * and only enlarge isize after a successful write. - */ - if (dio_data->updated_isize) { - u64 new_isize; + /* + * Got a short write and have updated the i_size, need to revert + * the i_size change. + * + * Normally we need to update i_size with extent lock held, but + * we're safe due to the following factors: + * + * - Only a single writer can be enlarging i_size + * Enlarging i_size will take the exclusive inode lock. + * + * - Buffered readers need to wait for the OE we're holding + * Buffered readers will lock extent and wait for OE + * of the folio range, and since page cache is invalidated + * the OE wait cannot be skipped. + * + * So here we are safe to revert the isize before finishing the + * OE, and no reader of the remaining range can see the enlarged + * size. + * + * TODO: Extend the DIO_LOCKED lifespan for direct writes, + * and only enlarge isize after a successful write. + */ + if (dio_data->updated_isize) { + u64 new_isize; - if (written == 0) - new_isize = dio_data->old_isize; - else - new_isize = max(dio_data->old_isize, pos); - i_size_write(inode, new_isize); - dio_data->updated_isize = false; - } - /* - * We have a short write, if there is any range - * that is submitted properly, that part will have - * its own OE split from the original one. - * - * So for the OE at dio_data->ordered, it's the part - * that is not submitted, and should be marked - * as fully truncated. - */ - btrfs_mark_ordered_extent_truncated(dio_data->ordered, 0); - btrfs_finish_ordered_extent(dio_data->ordered, - pos, length, true); - } else { - btrfs_unlock_dio_extent(&BTRFS_I(inode)->io_tree, pos, - pos + length - 1, NULL); + if (written == 0) + new_isize = dio_data->old_isize; + else + new_isize = max(dio_data->old_isize, pos + written); + i_size_write(inode, new_isize); + dio_data->updated_isize = false; } + /* + * We have a short write, if there is any range that is submitted + * properly, that part will have its own OE split from the + * original one. + * + * So for the OE at dio_data->ordered, it's the part that is not + * submitted, and should be marked as fully truncated. + */ + btrfs_mark_ordered_extent_truncated(dio_data->ordered, 0); + btrfs_finish_ordered_extent(dio_data->ordered, + pos + written, length - written, true); ret = -ENOTBLK; } - if (write) { - btrfs_put_ordered_extent(dio_data->ordered); - dio_data->ordered = NULL; - } - - if (write) - extent_changeset_free(dio_data->data_reserved); + btrfs_put_ordered_extent(dio_data->ordered); + dio_data->ordered = NULL; + extent_changeset_free(dio_data->data_reserved); return ret; } From 63a01c7578a655e560868fd47889b992418359bf Mon Sep 17 00:00:00 2001 From: Dongjiang Zhu Date: Tue, 9 Jun 2026 11:23:18 +0800 Subject: [PATCH 03/72] btrfs: start qgroup ioctl transactions on the quota root The qgroup ioctls update the quota tree, but they currently start their transactions using the root of the inode passed to the ioctl. This makes the transaction reservation depend on the path used for the ioctl instead of the tree being modified. Start qgroup ioctl transactions on the quota root instead. Take a reference to fs_info->quota_root under qgroup_ioctl_lock before starting the transaction, because quota disable can clear and put fs_info->quota_root after the early quota-enabled check. Keep the reference until the transaction handle is ended. Suggested-by: Qu Wenruo Reviewed-by: Qu Wenruo Signed-off-by: Dongjiang Zhu Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/ioctl.c | 50 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/fs/btrfs/ioctl.c b/fs/btrfs/ioctl.c index baa645e98812..6607e505dd68 100644 --- a/fs/btrfs/ioctl.c +++ b/fs/btrfs/ioctl.c @@ -3613,7 +3613,7 @@ static long btrfs_ioctl_qgroup_assign(struct file *file, void __user *arg) { struct inode *inode = file_inode(file); struct btrfs_fs_info *fs_info = inode_to_fs_info(inode); - struct btrfs_root *root = BTRFS_I(inode)->root; + struct btrfs_root *quota_root; struct btrfs_ioctl_qgroup_assign_args AUTO_KFREE(sa); struct btrfs_qgroup_list AUTO_KFREE(prealloc); struct btrfs_trans_handle *trans; @@ -3644,10 +3644,20 @@ static long btrfs_ioctl_qgroup_assign(struct file *file, void __user *arg) } } + mutex_lock(&fs_info->qgroup_ioctl_lock); + quota_root = btrfs_grab_root(fs_info->quota_root); + mutex_unlock(&fs_info->qgroup_ioctl_lock); + + if (!quota_root) { + ret = -ENOTCONN; + goto drop_write; + } + /* 2 BTRFS_QGROUP_RELATION_KEY items. */ - trans = btrfs_start_transaction(root, 2); + trans = btrfs_start_transaction(quota_root, 2); if (IS_ERR(trans)) { ret = PTR_ERR(trans); + btrfs_put_root(quota_root); goto drop_write; } @@ -3671,6 +3681,7 @@ static long btrfs_ioctl_qgroup_assign(struct file *file, void __user *arg) "qgroup status update failed after %s relation, marked as inconsistent", sa->assign ? "adding" : "deleting"); err = btrfs_end_transaction(trans); + btrfs_put_root(quota_root); if (err && !ret) ret = err; @@ -3682,7 +3693,8 @@ static long btrfs_ioctl_qgroup_assign(struct file *file, void __user *arg) static long btrfs_ioctl_qgroup_create(struct file *file, void __user *arg) { struct inode *inode = file_inode(file); - struct btrfs_root *root = BTRFS_I(inode)->root; + struct btrfs_fs_info *fs_info = inode_to_fs_info(inode); + struct btrfs_root *quota_root; struct btrfs_ioctl_qgroup_create_args AUTO_KFREE(sa); struct btrfs_trans_handle *trans; int ret; @@ -3691,7 +3703,7 @@ static long btrfs_ioctl_qgroup_create(struct file *file, void __user *arg) if (!capable(CAP_SYS_ADMIN)) return -EPERM; - if (!btrfs_qgroup_enabled(root->fs_info)) + if (!btrfs_qgroup_enabled(fs_info)) return -ENOTCONN; ret = mnt_want_write_file(file); @@ -3714,13 +3726,23 @@ static long btrfs_ioctl_qgroup_create(struct file *file, void __user *arg) goto drop_write; } + mutex_lock(&fs_info->qgroup_ioctl_lock); + quota_root = btrfs_grab_root(fs_info->quota_root); + mutex_unlock(&fs_info->qgroup_ioctl_lock); + + if (!quota_root) { + ret = -ENOTCONN; + goto drop_write; + } + /* * 1 BTRFS_QGROUP_INFO_KEY item. * 1 BTRFS_QGROUP_LIMIT_KEY item. */ - trans = btrfs_start_transaction(root, 2); + trans = btrfs_start_transaction(quota_root, 2); if (IS_ERR(trans)) { ret = PTR_ERR(trans); + btrfs_put_root(quota_root); goto drop_write; } @@ -3731,6 +3753,7 @@ static long btrfs_ioctl_qgroup_create(struct file *file, void __user *arg) } err = btrfs_end_transaction(trans); + btrfs_put_root(quota_root); if (err && !ret) ret = err; @@ -3743,6 +3766,8 @@ static long btrfs_ioctl_qgroup_limit(struct file *file, void __user *arg) { struct inode *inode = file_inode(file); struct btrfs_root *root = BTRFS_I(inode)->root; + struct btrfs_root *quota_root; + struct btrfs_fs_info *fs_info = root->fs_info; struct btrfs_ioctl_qgroup_limit_args AUTO_KFREE(sa); struct btrfs_trans_handle *trans; int ret; @@ -3752,7 +3777,7 @@ static long btrfs_ioctl_qgroup_limit(struct file *file, void __user *arg) if (!capable(CAP_SYS_ADMIN)) return -EPERM; - if (!btrfs_qgroup_enabled(root->fs_info)) + if (!btrfs_qgroup_enabled(fs_info)) return -ENOTCONN; ret = mnt_want_write_file(file); @@ -3765,10 +3790,20 @@ static long btrfs_ioctl_qgroup_limit(struct file *file, void __user *arg) goto drop_write; } + mutex_lock(&fs_info->qgroup_ioctl_lock); + quota_root = btrfs_grab_root(fs_info->quota_root); + mutex_unlock(&fs_info->qgroup_ioctl_lock); + + if (!quota_root) { + ret = -ENOTCONN; + goto drop_write; + } + /* 1 BTRFS_QGROUP_LIMIT_KEY item. */ - trans = btrfs_start_transaction(root, 1); + trans = btrfs_start_transaction(quota_root, 1); if (IS_ERR(trans)) { ret = PTR_ERR(trans); + btrfs_put_root(quota_root); goto drop_write; } @@ -3781,6 +3816,7 @@ static long btrfs_ioctl_qgroup_limit(struct file *file, void __user *arg) ret = btrfs_limit_qgroup(trans, qgroupid, &sa->lim); err = btrfs_end_transaction(trans); + btrfs_put_root(quota_root); if (err && !ret) ret = err; From 872df0c80f93d1da030863133f24fc3ed70c067c Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Mon, 8 Jun 2026 11:44:11 +0100 Subject: [PATCH 04/72] btrfs: don't over reserve metadata space for property in btrfs_fileattr_set() We are using 2 units for properties but we only set one property. Fix this by using the correct amount: 1 unit. Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/ioctl.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/btrfs/ioctl.c b/fs/btrfs/ioctl.c index 6607e505dd68..7e97a6aebf9b 100644 --- a/fs/btrfs/ioctl.c +++ b/fs/btrfs/ioctl.c @@ -393,9 +393,9 @@ int btrfs_fileattr_set(struct mnt_idmap *idmap, /* * 1 for inode item - * 2 for properties + * 1 for property */ - trans = btrfs_start_transaction(root, 3); + trans = btrfs_start_transaction(root, 2); if (IS_ERR(trans)) return PTR_ERR(trans); From 15f7c86215e8d5f14b24127fa88af6c79363d50e Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Tue, 9 Jun 2026 08:43:34 +0930 Subject: [PATCH 05/72] btrfs: do not overwrite NODATASUM flag when removing NODATACOW flag [TEST FAILURE] The test case generic/628 will fail if MOUNT_OPTIONS is set to "-o nodatasum": FSTYP -- btrfs PLATFORM -- Linux/x86_64 btrfs-vm 7.1.0-rc4-custom+ #383 SMP PREEMPT_DYNAMIC Sat May 30 07:35:42 ACST 2026 MKFS_OPTIONS -- -O bgt -K /dev/mapper/test-scratch1 MOUNT_OPTIONS -- -o nodatasum /dev/mapper/test-scratch1 /mnt/scratch generic/628 1s ... - output mismatch (see /home/adam/xfstests/results//generic/628.out.bad) --- tests/generic/628.out 2022-05-11 11:25:30.816666664 +0930 +++ /home/adam/xfstests/results//generic/628.out.bad 2026-06-08 18:56:49.878542927 +0930 @@ -8,8 +8,9 @@ 310f146ce52077fcd3308dcbe7632bb2 SCRATCH_MNT/a 310f146ce52077fcd3308dcbe7632bb2 SCRATCH_MNT/d test reflink flag not set iflag +XFS_IOC_CLONE: Invalid argument 310f146ce52077fcd3308dcbe7632bb2 SCRATCH_MNT/a -310f146ce52077fcd3308dcbe7632bb2 SCRATCH_MNT/b +d41d8cd98f00b204e9800998ecf8427e SCRATCH_MNT/b ... [CAUSE] The direct cause is that after "chattr +S", the btrfs inode will lose its NODATASUM flag inherited from the mount option. E.g.: # mkfs.btrfs -f $dev # mount $dev $mnt -o nodatasum # touch $mnt/foobar # sync # btrfs ins dump-tree -t 5 $dev | grep "(257 INODE_ITEM 0) itemoff" -A 3 item 4 key (257 INODE_ITEM 0) itemoff 15879 itemsize 160 generation 9 transid 9 size 0 nbytes 0 block group 0 mode 100644 links 1 uid 0 gid 0 rdev 0 sequence 1 flags 0x1(NODATASUM) ^^^^^^^^^ Proper NODATASUM flag # chattr +S $mnt/foobar # sync # btrfs ins dump-tree -t 5 $dev | grep "(257 INODE_ITEM 0) itemoff" -A 3 item 4 key (257 INODE_ITEM 0) itemoff 15879 itemsize 160 generation 9 transid 10 size 0 nbytes 0 block group 0 mode 100644 links 1 uid 0 gid 0 rdev 0 sequence 2 flags 0x20(SYNC) ^^^^ Only the new SYNC flag This makes the inode drop the old NODATASUM flag, while the new reflink destination will still inherit the NODATASUM flag. The mismatching NODATASUM flags will cause the reflink to fail. The root cause is that, inside btrfs_fileattr_set() if no FS_NOCOW_FL is set, we remove both NODATASUM and NODATACOW flag. However we should not touch NODATASUM flag, as data COW doesn't require checksum. Only NODATACOW implies NODATASUM, but DATACOW doesn't imply DATASUM. The deeper problems are: - Fileattr API is too binary It either clears or sets a flag, there is no "do not change" option. So that why "chattr +S" implies "chattr -C", and is forcing us to change NODATACOW along with NODATASUM flag. - No way to change NODATASUM through fileattr API In fact NODATASUM can only be modified through mount option. The deeper problems are much harder to attack. [FIX] Remove NODATACOW flag when FS_NOCOW_FL is not set, but only remove NODATASUM if "nodatasum" mount option is not set. This allows the existing "chattr +C" then "chattr -C" to remove both NODATACOW and NODATASUM flags on a default mount. But for a mount with "nodatasum" option, the NODATASUM inode flag will persist through either "chattr +C" and "chattr -C". Fixes: 7e97b8daf634 ("btrfs: allow setting NOCOW for a zero sized file via ioctl") Cc: stable@vger.kernel.org Reviewed-by: Filipe Manana Signed-off-by: Qu Wenruo Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/ioctl.c | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/fs/btrfs/ioctl.c b/fs/btrfs/ioctl.c index 7e97a6aebf9b..32dd7bbd4d63 100644 --- a/fs/btrfs/ioctl.c +++ b/fs/btrfs/ioctl.c @@ -356,14 +356,21 @@ int btrfs_fileattr_set(struct mnt_idmap *idmap, inode_flags |= BTRFS_INODE_NODATACOW; } } else { - /* - * Revert back under same assumptions as above - */ - if (S_ISREG(inode->vfs_inode.i_mode)) { - if (inode->vfs_inode.i_size == 0) - inode_flags &= ~(BTRFS_INODE_NODATACOW | - BTRFS_INODE_NODATASUM); - } else { + /* We can only change NODATACOW for zero-sized regular file. */ + if (S_ISREG(inode->vfs_inode.i_mode) && (inode->vfs_inode.i_size == 0)) { + inode_flags &= ~BTRFS_INODE_NODATACOW; + /* + * There is currently no way to change NODATASUM flag + * through fileattr API. If we unconditionally keep the + * current NODATASUM flag, chattr +C then chattr -C will + * keep the NODATASUM flag, and no way to remove that + * flag. + * + * So respect the current mount option for NODATASUM flag. + */ + if (!btrfs_test_opt(fs_info, NODATASUM)) + inode_flags &= ~BTRFS_INODE_NODATASUM; + } else if (!S_ISREG(inode->vfs_inode.i_mode)) { inode_flags &= ~BTRFS_INODE_NODATACOW; } } From 6f1c97695a6e230ee15c9e052e1933bdccfd5fa3 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Fri, 12 Jun 2026 12:02:14 +0100 Subject: [PATCH 06/72] btrfs: fix memory barrier order in reloc_root_is_dead() When we set a root's reloc_root to NULL, we do it like this: static void clear_reloc_root(struct btrfs_root *root) { root->reloc_root = NULL; /* * Need barrier to ensure clear_bit() only happens after * root->reloc_root = NULL. Pairs with have_reloc_root(). */ smp_wmb(); clear_bit(BTRFS_ROOT_DEAD_RELOC_TREE, &root->state); } So that a NULL reloc_root is always seen before seeing that the bit BTRFS_ROOT_DEAD_RELOC_TREE was cleared. But on the read side we have: static bool reloc_root_is_dead(const struct btrfs_root *root) { smp_rmb(); if (test_bit(BTRFS_ROOT_DEAD_RELOC_TREE, &root->state)) return true; return false; } And then callers of reloc_root_is_dead() access root->reloc_root. Because the read memory barrier is placed before testing the bit, the CPU is completely free to speculatively reorder those two loads. It can read root->reloc_root before it actually checks the dead tree bit. Sashiko reported this as an existing problem in another patch review, see the link in the Link tag below. Fix this by moving the read memory barrier to happen after testing the bit and update the comment to reflect current reality. Link: https://sashiko.dev/#/patchset/cf84f1a217c719e25b6b69e4298dd7afd36c9427.1781194426.git.fdmanana%40suse.com Reviewed-by: Boris Burkov Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/relocation.c | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/fs/btrfs/relocation.c b/fs/btrfs/relocation.c index fc5c14b5adad..4f83415ee8f8 100644 --- a/fs/btrfs/relocation.c +++ b/fs/btrfs/relocation.c @@ -339,14 +339,15 @@ static struct btrfs_backref_node *walk_down_backref( static bool reloc_root_is_dead(const struct btrfs_root *root) { - /* - * Pair with set_bit/clear_bit in clean_dirty_subvols and - * btrfs_update_reloc_root. We need to see the updated bit before - * trying to access reloc_root - */ - smp_rmb(); if (test_bit(BTRFS_ROOT_DEAD_RELOC_TREE, &root->state)) return true; + /* + * Pairs with set_bit/clear_bit in clear_reloc_root() and + * btrfs_update_reloc_root(). We need to see the updated bit before + * trying to access root->reloc_root in our callers. + */ + smp_rmb(); + return false; } From 0a9c35d3040d5aa6bb449b2f1e206cc0ac094343 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Fri, 12 Jun 2026 15:54:31 +0100 Subject: [PATCH 07/72] btrfs: fix copy_remapped_data() to not allocate more memory than intended The loop intends to copy the data in chunks up to 1M but we allocate the pages array for the entire length and don't cap it to 1M. Fix this by computing 'nr_pages' using 'copy_len' instead of 'length'. While at it, also make 'nr_pages' and 'copy_len' const, as they never change, to make the code more clear. Reviewed-by: Johannes Thumshirn Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/relocation.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/btrfs/relocation.c b/fs/btrfs/relocation.c index 4f83415ee8f8..6a1817613036 100644 --- a/fs/btrfs/relocation.c +++ b/fs/btrfs/relocation.c @@ -4115,10 +4115,10 @@ static int copy_remapped_data(struct btrfs_fs_info *fs_info, u64 old_addr, u64 new_addr, u64 length) { int ret; - u64 copy_len = min_t(u64, length, SZ_1M); + const u64 copy_len = min_t(u64, length, SZ_1M); struct page **pages; struct reloc_io_private priv; - unsigned int nr_pages = DIV_ROUND_UP(length, PAGE_SIZE); + const unsigned int nr_pages = DIV_ROUND_UP(copy_len, PAGE_SIZE); pages = kzalloc_objs(struct page *, nr_pages, GFP_NOFS); if (!pages) From 030d3514c6bb289cd3557a74b8d560dc0a26d8f8 Mon Sep 17 00:00:00 2001 From: Boris Burkov Date: Mon, 15 Jun 2026 10:40:59 -0700 Subject: [PATCH 08/72] btrfs: release extent lock per folio in readahead In Meta production, we have observed a large number of hosts running kernels newer than 6.13 which hit hung tasks on btrfs_read_folio()->lock_extents_for_read(). Looking through the history in this codepath reveals an interesting history. in 6.12, we merged commit ac325fc2aad5 ("btrfs: do not hold the extent lock for entire read") which holds the extent lock very narrowly while looking up the extent_map. However, this proved to introduce a serious race with DIO writes which was fixed in 6.14 with commit acc18e1c1d8c0 ("btrfs: fix stale page cache after race between readahead and direct IO write") That latter fix subtly changed the extent unlock point from the pre-6.12 regime. In 6.11, each read endio unlocked the extent it finished reading, but in 6.14, the extent is locked/unlocked as a unit around the entire readahead loop, while the individual folios are still unlocked as the endios finish. This is mostly the same behavior, as all successful reads will populate the page cache, so subsequent reads won't enter btrfs and hit the extent lock. But in the case where the readahead fails, perhaps because of a memory allocation failure doing compressed reads, the page will not be brought up to date and a later read of an overlapping range *will* block on the extent lock. Why is this a problem? On sufficiently large loaded systems, I have observed that direct reclaim can run for minutes. Given that, consider two tasks on such a system reading an overlapping range of a compressed file: Task 1 locks the whole range and starts to read. Some allocation for the compressed read for folio F fails and we carry on while holding the extent lock for the full range. Task 2 wants to read F, which is not uptodate and in page cache, so it blocks on the extent lock held by Task 1. Task 1 keeps getting stuck in direct reclaim (likely, we already supposed an allocation failure above) Task 2 stays blocked on the extent lock the whole time. If you consider the effects of readahead_expand and imagine a file with a 128k compressed extent followed by many smaller compressed extents, you can imagine that the expanded window will result in subsequent reads hitting many extents (128k/4k = 32) per lock window in the worst case. The system likeley wouldn't be all that healthy anyway, so this is likely not a critical improvement, but it does alleviate this one source of stress and one thread's slowdown escalating to others. To bring this behavior back to the old model, we should unlock the extent at each loop of the readahead loop rather than in one shot at the end. This allows such overlapping reads to proceed as they should. Writes are fine because either the page has already been read and has an appropriate state in the page cache to be invalidated (or not uptodate) or it is still-to-be-read and the extent lock is still held protecting it. Reviewed-by: Filipe Manana Reviewed-by: Qu Wenruo Signed-off-by: Boris Burkov Signed-off-by: David Sterba --- fs/btrfs/extent_io.c | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c index f032f0858f40..eadd8d205411 100644 --- a/fs/btrfs/extent_io.c +++ b/fs/btrfs/extent_io.c @@ -2858,12 +2858,23 @@ void btrfs_readahead(struct readahead_control *rac) struct fsverity_info *vi = NULL; lock_extents_for_read(inode, start, end, &cached_state); + /* We don't use cached state for a bulk unlock, just free it. */ + btrfs_free_extent_state(cached_state); if (start < i_size_read(vfs_inode)) vi = fsverity_get_info(vfs_inode); - while ((folio = readahead_folio(rac)) != NULL) - btrfs_do_readpage(folio, &em_cached, &bio_ctrl, vi); + while ((folio = readahead_folio(rac)) != NULL) { + /* + * Read start and end before btrfs_do_readpage(). It unlocks the + * folio, so our reference might not be valid after. + */ + const u64 folio_start = folio_pos(folio); + const u64 folio_end = folio_start + folio_size(folio) - 1; - btrfs_unlock_extent(&inode->io_tree, start, end, &cached_state); + btrfs_do_readpage(folio, &em_cached, &bio_ctrl, vi); + /* Only unlock the range we locked, even if readahead expands. */ + if (folio_start >= start && folio_end <= end) + btrfs_unlock_extent(&inode->io_tree, folio_start, folio_end, NULL); + } if (em_cached) btrfs_free_extent_map(em_cached); From 0d99b3b1cfa8d5d4a5c0a6d693b52d5d40775f7f Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Tue, 16 Jun 2026 09:24:22 +0930 Subject: [PATCH 09/72] btrfs: log swapfile activation/deactivation and warn about pinned block groups A swap file on btrfs will pin down block groups that cover the swap file extent. Pinned down block groups will be skipped for scrub and relocation. These degradation on critical btrfs maintenance operations is never properly educated to end users, and have already caused problems including: - Scrub finished too quick Because the enabled swap file has pinned down most of the block groups. Thus any file extents in those block groups, even not utilized by the swap file, will be skipped from scrub. - Unbalanced data and metadata usage, meanwhile relocation won't help The same reason, pinned down block groups will not be considered as relocation target, thus data extents that are not utilized by the swap file can still be skipped from relocation. Although we already have kernel messages for both scrub and balance, the balance one is still info level. To better communicate those potential long term problems, add the following output into dmesg: - Change the message level to warn for __btrfs_balance() - Total pinned down block group number and size during swapfile activation - Total released block group number and size during swapfile deactivation The above messages have info level. - The fact that pinned down block groups will not be scrubbed nor balanced The above message has warning level. The example output would look like the following, for enabling a 1.2G swapfile, which pinned down 2G block groups: BTRFS info (device dm-3): swapfile activated on root 5 ino 257, pinned down 2147483648 bytes from 2 block group(s) BTRFS warning (device dm-3): block groups with swapfile extents will not be scrubbed or balanced Adding 1257468k swap on /mnt/btrfs/foobar. Priority:-1 extents:1 across:1257468k BTRFS info (device dm-3): swapfile deactivated on root 5 ino 257, released 2147483648 bytes from 2 block group(s) Reviewed-by: Filipe Manana Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/inode.c | 28 ++++++++++++++++++++++++++-- fs/btrfs/volumes.c | 2 +- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/fs/btrfs/inode.c b/fs/btrfs/inode.c index 2534cd9284d5..914d690107ae 100644 --- a/fs/btrfs/inode.c +++ b/fs/btrfs/inode.c @@ -10194,6 +10194,8 @@ static void btrfs_free_swapfile_pins(struct inode *inode) struct btrfs_fs_info *fs_info = BTRFS_I(inode)->root->fs_info; struct btrfs_swapfile_pin *sp; struct rb_node *node, *next; + u64 bg_bytes_released = 0; + u32 bg_nr_released = 0; spin_lock(&fs_info->swapfile_pins_lock); node = rb_first(&fs_info->swapfile_pins); @@ -10203,15 +10205,24 @@ static void btrfs_free_swapfile_pins(struct inode *inode) if (sp->inode == inode) { rb_erase(&sp->node, &fs_info->swapfile_pins); if (sp->is_block_group) { - btrfs_dec_block_group_swap_extents(sp->ptr, + struct btrfs_block_group *bg = sp->ptr; + + bg_bytes_released += bg->length; + bg_nr_released++; + btrfs_dec_block_group_swap_extents(bg, sp->bg_extent_count); - btrfs_put_block_group(sp->ptr); + btrfs_put_block_group(bg); } kfree(sp); } node = next; } spin_unlock(&fs_info->swapfile_pins_lock); + btrfs_info(fs_info, +"swapfile deactivated on root %llu ino %llu, released %llu bytes from %u block group(s)", + btrfs_root_id(BTRFS_I(inode)->root), + btrfs_ino(BTRFS_I(inode)), bg_bytes_released, + bg_nr_released); } struct btrfs_swap_info { @@ -10289,8 +10300,10 @@ static int btrfs_swap_activate(struct swap_info_struct *sis, struct file *file, struct btrfs_backref_share_check_ctx *backref_ctx = NULL; struct btrfs_path *path = NULL; int ret = 0; + u32 pinned_bg_nr = 0; u64 isize; u64 prev_extent_end = 0; + u64 pinned_bg_size = 0; /* * Acquire the inode's mmap lock to prevent races with memory mapped @@ -10540,6 +10553,9 @@ static int btrfs_swap_activate(struct swap_info_struct *sis, struct file *file, ret = 0; else goto out; + } else { + pinned_bg_size += bg->length; + pinned_bg_nr++; } if (bsi.block_len && @@ -10587,6 +10603,14 @@ static int btrfs_swap_activate(struct swap_info_struct *sis, struct file *file, if (ret) return ret; + btrfs_info(fs_info, +"swapfile activated on root %llu ino %llu, pinned down %llu bytes from %u block group(s)", + btrfs_root_id(BTRFS_I(inode)->root), + btrfs_ino(BTRFS_I(inode)), + pinned_bg_size, pinned_bg_nr); + btrfs_warn(fs_info, +"block groups with swapfile extents will not be scrubbed or balanced"); + if (device) sis->bdev = device->bdev; *span = bsi.highest_ppage - bsi.lowest_ppage + 1; diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c index 6eab4cc73ce4..2d132c827913 100644 --- a/fs/btrfs/volumes.c +++ b/fs/btrfs/volumes.c @@ -4588,7 +4588,7 @@ static int __btrfs_balance(struct btrfs_fs_info *fs_info) if (ret == -ENOSPC) { enospc_errors++; } else if (ret == -ETXTBSY) { - btrfs_info(fs_info, + btrfs_warn(fs_info, "skipping relocation of block group %llu due to active swapfile", found_key.offset); ret = 0; From 545e560a5b0fb97ff4154e6fb0674d1f5055a81e Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Tue, 9 Jun 2026 19:31:44 +0930 Subject: [PATCH 10/72] btrfs: disable v1 space cache V2 space cache has been the default mkfs option since btrfs-progs v5.15, and commit 1e7bec1f7d65 ("btrfs: emit a warning about space cache v1 being deprecated") has already added a warning to show v1 space cache has been deprecated. It has been long enough that we should remove v1 space cache completely. As the first step, disable v1 space cache by: - Make "space_cache" mount option fallback to "nospace_cache" - Make "space_cache=v1" fall back to "nospace_cache" This is safer than forcing "space_cache=v2", as forcing v2 cache requires removal of v1 cache and regenerating v2 cache. Such operation can be slow, and takes extra metadata space, thus it is not always safe for existing filesystems. With this done, v1 cache mount will always fallback to nospace cache, and mount option will not be able to force v1 space cache usage. For example, even for a fs with v1 cache: # btrfs ins dump-super test.img superblock: bytenr=65536, device=test.img --------------------------------------------------------- csum_type 0 (crc32c) csum_size 4 csum 0xdce44b2c [match] bytenr 65536 flags 0x1 ( WRITTEN ) magic _BHRfS_M [match] fsid 7d7c3bba-8211-4206-868d-10eedd5703f8 metadata_uuid 00000000-0000-0000-0000-000000000000 label generation 9 root 30605312 [...] compat_ro_flags 0x0 <<< No FST feature incompat_flags 0x361 ( MIXED_BACKREF | BIG_METADATA | EXTENDED_IREF | SKINNY_METADATA | NO_HOLES ) cache_generation 9 <<< Matches generation uuid_tree_generation 9 Attempting to mount it will lead to no space cache other than v1 space cache: # mount test.img /mnt/btrfs # dmesg -t | tail -n 5 BTRFS: device fsid 7d7c3bba-8211-4206-868d-10eedd5703f8 devid 1 transid 9 /dev/loop0 (7:0) scanned by mount (1264) BTRFS info (device loop0): first mount of filesystem 7d7c3bba-8211-4206-868d-10eedd5703f8 BTRFS info (device loop0): using crc32c checksum algorithm BTRFS info (device loop0): turning on async discard BTRFS info (device loop0): last unmount of filesystem 7d7c3bba-8211-4206-868d-10eedd5703f8 Even forcing v1 cache will not work, but fallback to the usual nospace_cache: # mount test.img -o space_cache=v1 /mnt/btrfs # dmesg -t | tail -n 6 BTRFS warning: v1 space cache is deprecated, fallback to no space cache BTRFS: device fsid 7d7c3bba-8211-4206-868d-10eedd5703f8 devid 1 transid 9 /dev/loop0 (7:0) scanned by mount (1264) BTRFS info (device loop0): first mount of filesystem 7d7c3bba-8211-4206-868d-10eedd5703f8 BTRFS info (device loop0): using crc32c checksum algorithm BTRFS info (device loop0): turning on async discard BTRFS info (device loop0): last unmount of filesystem 7d7c3bba-8211-4206-868d-10eedd5703f8 And there will be no way to force converting a v2 cache back to v1, such attempt will only clear free space tree and fallback to no space cache. # mkfs.btrfs -f -O fst,^bgt test.img # mount -o clear_cache,space_cache=v1 test.img /mnt/btrfs # dmesg -t | tail -n 11 BTRFS warning: v1 space cache is deprecated, fallback to no space cache BTRFS: device fsid f59daad2-3ab5-4f33-b752-a36cfb09b674 devid 1 transid 8 /dev/loop0 (7:0) scanned by mount (1419) BTRFS info (device loop0): first mount of filesystem f59daad2-3ab5-4f33-b752-a36cfb09b674 BTRFS info (device loop0): using crc32c checksum algorithm BTRFS info (device loop0): rebuilding free space tree BTRFS info (device loop0): disabling free space tree BTRFS info (device loop0): clearing compat-ro feature flag for FREE_SPACE_TREE (0x1) BTRFS info (device loop0): clearing compat-ro feature flag for FREE_SPACE_TREE_VALID (0x2) BTRFS info (device loop0): checking UUID tree BTRFS info (device loop0): turning on async discard BTRFS info (device loop0): force clearing of disk cache # mount | grep /mnt/btrfs /mnt/test.img on /mnt/btrfs type btrfs (rw,relatime,discard=async,nospace_cache,subvolid=5,subvol=/) Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/super.c | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/fs/btrfs/super.c b/fs/btrfs/super.c index f4e34898d581..41658705b4e9 100644 --- a/fs/btrfs/super.c +++ b/fs/btrfs/super.c @@ -514,19 +514,20 @@ static int btrfs_parse_param(struct fs_context *fc, struct fs_parameter *param) btrfs_clear_opt(ctx->mount_opt, NODISCARD); break; case Opt_space_cache: - if (result.negated) { - btrfs_set_opt(ctx->mount_opt, NOSPACECACHE); - btrfs_clear_opt(ctx->mount_opt, SPACE_CACHE); - btrfs_clear_opt(ctx->mount_opt, FREE_SPACE_TREE); - } else { - btrfs_clear_opt(ctx->mount_opt, FREE_SPACE_TREE); - btrfs_set_opt(ctx->mount_opt, SPACE_CACHE); - } + if (!result.negated) + btrfs_warn(NULL, + "v1 space cache is deprecated, falling back to no space cache"); + btrfs_set_opt(ctx->mount_opt, NOSPACECACHE); + btrfs_clear_opt(ctx->mount_opt, SPACE_CACHE); + btrfs_clear_opt(ctx->mount_opt, FREE_SPACE_TREE); break; case Opt_space_cache_version: switch (result.uint_32) { case Opt_space_cache_v1: - btrfs_set_opt(ctx->mount_opt, SPACE_CACHE); + btrfs_warn(NULL, + "v1 space cache is deprecated, falling back to no space cache"); + btrfs_set_opt(ctx->mount_opt, NOSPACECACHE); + btrfs_clear_opt(ctx->mount_opt, SPACE_CACHE); btrfs_clear_opt(ctx->mount_opt, FREE_SPACE_TREE); break; case Opt_space_cache_v2: From 6eb2fe0724bd235f6f83d28c8f9c22666854bf2a Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Fri, 19 Jun 2026 18:24:46 +0930 Subject: [PATCH 11/72] btrfs: remove out-of-date comments regarding 2K block size Since commit bac3c2910c0c ("btrfs: remove 2K block size support") there is no 2K block size support inside btrfs anymore. Remove the stale comments of btrfs_supported_blocksize(). Reviewed-by: Johannes Thumshirn Signed-off-by: Qu Wenruo Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/fs.c | 6 ------ 1 file changed, 6 deletions(-) diff --git a/fs/btrfs/fs.c b/fs/btrfs/fs.c index dcf12979af33..5bd6856213aa 100644 --- a/fs/btrfs/fs.c +++ b/fs/btrfs/fs.c @@ -135,12 +135,6 @@ void btrfs_csum_final(struct btrfs_csum_ctx *ctx, u8 *out) * * - PAGE_SIZE * The straightforward block size to support. - * - * And extra support for the following block sizes based on the kernel config: - * - * - MIN_BLOCKSIZE - * This is either 4K (regular builds) or 2K (debug builds) - * This allows testing subpage routines on x86_64. */ bool __attribute_const__ btrfs_supported_blocksize(u32 blocksize) { From 097cdc84620e7dbc8653bddb4a3009e08980ea1f Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Fri, 19 Jun 2026 18:24:47 +0930 Subject: [PATCH 12/72] btrfs: allow any block size that is no larger than page size Since v5.15 btrfs has support for block size < page size, but we still only support 4K block size, while there is no special reason that we cannot support 8K/16K/32K block sizes for 64K page size. That 4K limit is completely arbitrary, and mostly to reduce test runtime so we do not need to test all the extra block size combinations. However that also limits the user choices, some users may understand what they are doing, and want larger block sizes. In that case, fixed 4K block size for subpage routine is blocking our way. Just remove that fixed 4K requirement for block size < page size. This should not affect regular end users, since mkfs is already using 4K block size as default for quite a while, and the existing bs == ps support is always there. But for power users, this allows extra block size support, and may provide extra test coverage. Reviewed-by: Johannes Thumshirn Signed-off-by: Qu Wenruo Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/fs.c | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/fs/btrfs/fs.c b/fs/btrfs/fs.c index 5bd6856213aa..de160d29dde8 100644 --- a/fs/btrfs/fs.c +++ b/fs/btrfs/fs.c @@ -127,14 +127,9 @@ void btrfs_csum_final(struct btrfs_csum_ctx *ctx, u8 *out) } /* - * We support the following block sizes for all systems: - * - * - 4K - * This is the most common block size. For PAGE SIZE > 4K cases the subpage - * mode is used. - * - * - PAGE_SIZE - * The straightforward block size to support. + * For regular builds, any block size <= page size is supported. + * For experimental builds, any block size between BTRFS_MIN_BLOCKSIZE + * and BTRFS_MAX_BLOCKSIZE (inclusive) is supported. */ bool __attribute_const__ btrfs_supported_blocksize(u32 blocksize) { @@ -142,7 +137,7 @@ bool __attribute_const__ btrfs_supported_blocksize(u32 blocksize) ASSERT(is_power_of_2(blocksize) && blocksize >= BTRFS_MIN_BLOCKSIZE && blocksize <= BTRFS_MAX_BLOCKSIZE); - if (blocksize == PAGE_SIZE || blocksize == SZ_4K || blocksize == BTRFS_MIN_BLOCKSIZE) + if (blocksize <= PAGE_SIZE) return true; #ifdef CONFIG_BTRFS_EXPERIMENTAL /* From e549093c11a2fff8430df3dfbdb45eb9811a69a5 Mon Sep 17 00:00:00 2001 From: Johannes Thumshirn Date: Tue, 23 Jun 2026 13:38:51 +0200 Subject: [PATCH 13/72] btrfs: zoned: don't force read-only on transient -EAGAIN from reloc merge On a zoned FS, btrfs_delayed_refs_rsv_refill() returns -EAGAIN whenever the over-committed metadata plus the zone_unusable bytes exceeds the usable size in a metadata block-group to avoid heavy over-commit of metadata and early ENOSPC in one transaction. If this happens while doing reclaim, the transaction is getting aborted. Treat -EAGAIN as a soft, retryable condition in case of block-group reclaim. Reported-by: Damien Le Moal Fixes: 7bcb04de982f ("btrfs: zoned: cap delayed refs metadata reservation to avoid overcommit") Reviewed-by: Filipe Manana Signed-off-by: Johannes Thumshirn Signed-off-by: David Sterba --- fs/btrfs/block-group.c | 8 ++++++- fs/btrfs/relocation.c | 49 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/fs/btrfs/block-group.c b/fs/btrfs/block-group.c index 8def7abb728f..c5bcd3c03d24 100644 --- a/fs/btrfs/block-group.c +++ b/fs/btrfs/block-group.c @@ -2047,6 +2047,11 @@ static int btrfs_reclaim_block_group(struct btrfs_block_group *bg, int *reclaime trace_btrfs_reclaim_block_group(bg); ret = btrfs_relocate_chunk(fs_info, bg->start, false); + if (btrfs_is_zoned(fs_info) && ret == -EAGAIN) { + btrfs_dec_block_group_ro(bg); + btrfs_debug(fs_info, "deferring reclaim of chunk %llu", bg->start); + return ret; + } if (ret) { btrfs_dec_block_group_ro(bg); btrfs_err(fs_info, "error relocating chunk %llu", @@ -2113,7 +2118,8 @@ void btrfs_reclaim_block_groups(struct btrfs_fs_info *fs_info, unsigned int limi spin_unlock(&fs_info->unused_bgs_lock); ret = btrfs_reclaim_block_group(bg, &reclaimed); - if (ret && !READ_ONCE(space_info->periodic_reclaim)) + if ((btrfs_is_zoned(fs_info) && ret == -EAGAIN) || + (ret && !READ_ONCE(space_info->periodic_reclaim))) btrfs_link_bg_list(bg, &retry_list); btrfs_put_block_group(bg); diff --git a/fs/btrfs/relocation.c b/fs/btrfs/relocation.c index 6a1817613036..f14bb4158d8d 100644 --- a/fs/btrfs/relocation.c +++ b/fs/btrfs/relocation.c @@ -1538,6 +1538,33 @@ static void clear_reloc_root(struct btrfs_root *root) clear_bit(BTRFS_ROOT_DEAD_RELOC_TREE, &root->state); } +/* Drop the reloc trees of a relocation that is being deferred and retried. */ +static void abort_reloc_roots(struct reloc_control *rc, struct list_head *list) +{ + struct btrfs_fs_info *fs_info = rc->extent_root->fs_info; + struct btrfs_root *reloc_root, *tmp; + + list_for_each_entry_safe(reloc_root, tmp, list, root_list) { + struct btrfs_root *root; + + root = btrfs_get_fs_root(fs_info, reloc_root->root_key.offset, false); + if (!IS_ERR(root)) { + if (root->reloc_root == reloc_root) { + clear_reloc_root(root); + btrfs_put_root(reloc_root); + } + btrfs_put_root(root); + } + + btrfs_set_root_refs(&reloc_root->root_item, 0); + memset(&reloc_root->root_item.drop_progress, 0, sizeof(struct btrfs_disk_key)); + btrfs_set_root_drop_level(&reloc_root->root_item, 0); + + list_del_init(&reloc_root->root_list); + list_add_tail(&reloc_root->reloc_dirty_list, &rc->dirty_subvol_roots); + } +} + static int clean_dirty_subvols(struct reloc_control *rc) { struct btrfs_root *root; @@ -1877,8 +1904,7 @@ int prepare_to_merge(struct reloc_control *rc, int err) return err; } -static noinline_for_stack -void merge_reloc_roots(struct reloc_control *rc) +static noinline_for_stack int merge_reloc_roots(struct reloc_control *rc) { struct btrfs_fs_info *fs_info = rc->extent_root->fs_info; struct btrfs_root *root; @@ -1976,7 +2002,15 @@ void merge_reloc_roots(struct reloc_control *rc) goto again; } out: - if (ret) { + if (btrfs_is_zoned(fs_info) && ret == -EAGAIN) { + abort_reloc_roots(rc, &reloc_roots); + + /* New reloc root may be added. */ + mutex_lock(&fs_info->reloc_mutex); + list_splice_init(&rc->reloc_roots, &reloc_roots); + mutex_unlock(&fs_info->reloc_mutex); + abort_reloc_roots(rc, &reloc_roots); + } else if (ret) { btrfs_handle_fs_error(fs_info, ret, NULL); free_reloc_roots(&reloc_roots); @@ -2002,6 +2036,7 @@ void merge_reloc_roots(struct reloc_control *rc) * * The remaining nodes will be cleaned up by put_reloc_control(). */ + return ret; } static void free_block_list(struct rb_root *blocks) @@ -3731,7 +3766,9 @@ static noinline_for_stack int relocate_block_group(struct reloc_control *rc) */ err = prepare_to_merge(rc, err); - merge_reloc_roots(rc); + ret = merge_reloc_roots(rc); + if (ret && !err) + err = ret; rc->merge_reloc_tree = false; unset_reloc_control(rc); @@ -5700,7 +5737,9 @@ int btrfs_recover_relocation(struct btrfs_fs_info *fs_info) if (ret) goto out_unset; - merge_reloc_roots(rc); + ret = merge_reloc_roots(rc); + if (ret) + goto out_unset; unset_reloc_control(rc); From a4cea1272c2068cc109f561f2f00176d92244da7 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Mon, 15 Jun 2026 17:05:22 +0100 Subject: [PATCH 14/72] btrfs: send: fix comment for SEND_MAX_DIR_UTIMES_CACHE_SIZE The comment is wrong, because it's not about storing the ID of new directories that were already created, instead it's about storing utimes values for directories (both new and existing). The comment is wrong because it was copy pasted from SEND_MAX_DIR_CREATED_CACHE_SIZE, but forgot to update it afterwards. Reviewed-by: Johannes Thumshirn Reviewed-by: Daniel Vacek Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/send.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fs/btrfs/send.c b/fs/btrfs/send.c index 3ae480c7474b..d37c18f41545 100644 --- a/fs/btrfs/send.c +++ b/fs/btrfs/send.c @@ -130,10 +130,10 @@ static_assert(offsetof(struct backref_cache_entry, entry) == 0); #define SEND_MAX_DIR_CREATED_CACHE_SIZE 64 /* - * Max number of entries in the cache that stores directories that were already - * created. The cache uses raw struct btrfs_lru_cache_entry entries, so it uses - * at most 4096 bytes - sizeof(struct btrfs_lru_cache_entry) is 48 bytes, but - * the kmalloc-64 slab is used, so we get 4096 bytes (64 bytes * 64). + * Maximum number of entries in the cache that stores utimes values for directories. + * The cache uses raw struct btrfs_lru_cache_entry entries, so it uses at most + * 4096 bytes - sizeof(struct btrfs_lru_cache_entry) is 48 bytes, but the + * kmalloc-64 slab is used, so we get 4096 bytes (64 bytes * 64). */ #define SEND_MAX_DIR_UTIMES_CACHE_SIZE 64 From bd3dddec1b78bf823b1966aff3ed3f9bb0ebe39c Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Mon, 15 Jun 2026 17:33:03 +0100 Subject: [PATCH 15/72] btrfs: send: fix is_current_inode_path() to avoid path resets for common prefixes In case the current inode's path is a prefix of the given path, the helper is_current_inode_path() will return true, which causes the single caller to reset the current inode's path. While this is not a functional issue, it makes the caller recompute the current inode's path later. It could also become a problem in the future in case get new callers for is_current_inode_path() in more sensitive contexts. Example: the current inode path is "/foo/bar" and the path we compare against is "/foo/bar_xyz". Fix this by returning true only if we have exact matches. Reviewed-by: Johannes Thumshirn Reviewed-by: Daniel Vacek Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/send.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/fs/btrfs/send.c b/fs/btrfs/send.c index d37c18f41545..1023ab3b5840 100644 --- a/fs/btrfs/send.c +++ b/fs/btrfs/send.c @@ -625,9 +625,8 @@ static void fs_path_unreverse(struct fs_path *p) static inline bool is_current_inode_path(const struct send_ctx *sctx, const struct fs_path *path) { - const struct fs_path *cur = &sctx->cur_inode_path; - - return (strncmp(path->start, cur->start, fs_path_len(cur)) == 0); + /* Paths are always nul terminated. */ + return (strcmp(path->start, sctx->cur_inode_path.start) == 0); } static struct btrfs_path *alloc_path_for_send(void) From f1df52e323b2d934194f0bd47827cd214105ce23 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Wed, 24 Jun 2026 13:42:42 +0930 Subject: [PATCH 16/72] btrfs: use correct type for sectorsize/nodesize/blocksize The nodesize and sectorsize are all u32 values, there is no need to use u64 for local usage. Furthermore some call sites also use "blocksize" or "bs" for sectorsize, also change them to use the minimal type u32 instead. Reviewed-by: Boris Burkov Signed-off-by: Qu Wenruo Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/disk-io.c | 12 ++++++------ fs/btrfs/fiemap.c | 2 +- fs/btrfs/file.c | 4 ++-- fs/btrfs/inode.c | 4 ++-- fs/btrfs/reflink.c | 6 +++--- fs/btrfs/send.c | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c index 2f1666d9544e..d12c818bfbf1 100644 --- a/fs/btrfs/disk-io.c +++ b/fs/btrfs/disk-io.c @@ -2395,8 +2395,8 @@ static int validate_sys_chunk_array(const struct btrfs_fs_info *fs_info, int btrfs_validate_super(const struct btrfs_fs_info *fs_info, const struct btrfs_super_block *sb, int mirror_num) { - u64 nodesize = btrfs_super_nodesize(sb); - u64 sectorsize = btrfs_super_sectorsize(sb); + const u32 nodesize = btrfs_super_nodesize(sb); + const u32 sectorsize = btrfs_super_sectorsize(sb); int ret = 0; const bool ignore_flags = btrfs_test_opt(fs_info, IGNORESUPERFLAGS); @@ -2438,24 +2438,24 @@ int btrfs_validate_super(const struct btrfs_fs_info *fs_info, */ if (unlikely(!is_power_of_2(sectorsize) || sectorsize < BTRFS_MIN_BLOCKSIZE || sectorsize > BTRFS_MAX_METADATA_BLOCKSIZE)) { - btrfs_err(fs_info, "invalid sectorsize %llu", sectorsize); + btrfs_err(fs_info, "invalid sectorsize %u", sectorsize); ret = -EINVAL; } if (unlikely(!btrfs_supported_blocksize(sectorsize))) { btrfs_err(fs_info, - "sectorsize %llu not yet supported for page size %lu", + "sectorsize %u not yet supported for page size %lu", sectorsize, PAGE_SIZE); ret = -EINVAL; } if (unlikely(!is_power_of_2(nodesize) || nodesize < sectorsize || nodesize > BTRFS_MAX_METADATA_BLOCKSIZE)) { - btrfs_err(fs_info, "invalid nodesize %llu", nodesize); + btrfs_err(fs_info, "invalid nodesize %u", nodesize); ret = -EINVAL; } if (unlikely(nodesize != le32_to_cpu(sb->__unused_leafsize))) { - btrfs_err(fs_info, "invalid leafsize %u, should be %llu", + btrfs_err(fs_info, "invalid leafsize %u, should be %u", le32_to_cpu(sb->__unused_leafsize), nodesize); ret = -EINVAL; } diff --git a/fs/btrfs/fiemap.c b/fs/btrfs/fiemap.c index 6263e837093e..ba6a360074c0 100644 --- a/fs/btrfs/fiemap.c +++ b/fs/btrfs/fiemap.c @@ -641,7 +641,7 @@ static int extent_fiemap(struct btrfs_inode *inode, u64 prev_extent_end; u64 range_start; u64 range_end; - const u64 sectorsize = inode->root->fs_info->sectorsize; + const u32 sectorsize = inode->root->fs_info->sectorsize; bool stopped = false; int ret; diff --git a/fs/btrfs/file.c b/fs/btrfs/file.c index a2a2df2df786..b46c89771a9f 100644 --- a/fs/btrfs/file.c +++ b/fs/btrfs/file.c @@ -2888,7 +2888,7 @@ enum { static int btrfs_zero_range_check_range_boundary(struct btrfs_inode *inode, u64 offset) { - const u64 sectorsize = inode->root->fs_info->sectorsize; + const u32 sectorsize = inode->root->fs_info->sectorsize; struct extent_map *em; int ret; @@ -2918,7 +2918,7 @@ static int btrfs_zero_range(struct inode *inode, struct extent_changeset *data_reserved = NULL; int ret; u64 alloc_hint = 0; - const u64 sectorsize = fs_info->sectorsize; + const u32 sectorsize = fs_info->sectorsize; const u64 orig_start = offset; const u64 orig_end = offset + len - 1; u64 alloc_start = round_down(offset, sectorsize); diff --git a/fs/btrfs/inode.c b/fs/btrfs/inode.c index 914d690107ae..b0693065a0c7 100644 --- a/fs/btrfs/inode.c +++ b/fs/btrfs/inode.c @@ -860,7 +860,7 @@ static void compress_file_range(struct btrfs_work *work) struct btrfs_inode *inode = async_chunk->inode; struct btrfs_fs_info *fs_info = inode->root->fs_info; struct compressed_bio *cb = NULL; - u64 blocksize = fs_info->sectorsize; + const u32 blocksize = fs_info->sectorsize; u64 start = async_chunk->start; u64 end = async_chunk->end; u64 actual_end; @@ -3039,7 +3039,7 @@ static int insert_reserved_file_extent(struct btrfs_trans_handle *trans, u64 qgroup_reserved) { struct btrfs_root *root = inode->root; - const u64 sectorsize = root->fs_info->sectorsize; + const u32 sectorsize = root->fs_info->sectorsize; BTRFS_PATH_AUTO_FREE(path); struct extent_buffer *leaf; struct btrfs_key ins; diff --git a/fs/btrfs/reflink.c b/fs/btrfs/reflink.c index 9a49d2ecb949..28bb05a92106 100644 --- a/fs/btrfs/reflink.c +++ b/fs/btrfs/reflink.c @@ -691,7 +691,7 @@ static int btrfs_extent_same_range(struct btrfs_inode *src, u64 loff, u64 len, const u64 end = dst_loff + len - 1; struct extent_state *cached_state = NULL; struct btrfs_fs_info *fs_info = src->root->fs_info; - const u64 bs = fs_info->sectorsize; + const u32 bs = fs_info->sectorsize; int ret; /* @@ -762,7 +762,7 @@ static noinline int btrfs_clone_files(struct file *file, struct file *file_src, struct btrfs_fs_info *fs_info = inode_to_fs_info(inode); int ret; u64 len = olen; - u64 bs = fs_info->sectorsize; + const u32 bs = fs_info->sectorsize; u64 end; /* @@ -841,7 +841,7 @@ static int btrfs_remap_file_range_prep(struct file *file_in, loff_t pos_in, { struct btrfs_inode *inode_in = BTRFS_I(file_inode(file_in)); struct btrfs_inode *inode_out = BTRFS_I(file_inode(file_out)); - u64 bs = inode_out->root->fs_info->sectorsize; + const u32 bs = inode_out->root->fs_info->sectorsize; u64 wb_len; int ret; diff --git a/fs/btrfs/send.c b/fs/btrfs/send.c index 1023ab3b5840..02a1450bf710 100644 --- a/fs/btrfs/send.c +++ b/fs/btrfs/send.c @@ -6032,7 +6032,7 @@ static int send_write_or_clone(struct send_ctx *sctx, int ret = 0; u64 offset = key->offset; u64 end; - u64 bs = sctx->send_root->fs_info->sectorsize; + const u32 bs = sctx->send_root->fs_info->sectorsize; struct btrfs_file_extent_item *ei; u64 disk_byte; u64 data_offset; From 49a75e200d1c74357e012e1e351ae8b10ed79d11 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Wed, 24 Jun 2026 13:53:28 +0930 Subject: [PATCH 17/72] btrfs: remove btrfs_fs_info::stripesize Btrfs does not support variable stripe length yet, all RAID0/5/6/10 chunks have the fixed stripe length 64K for now. Furthermore, btrfs_fs_info::stripesize is not the real chunk stripe length, it's always the same value as sectorsize. Remove btrfs_fs_info::stripesize, and for the only callsite utilizing that member, replace it with fs_info->sectorsize instead. Signed-off-by: Qu Wenruo Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/disk-io.c | 4 ---- fs/btrfs/extent-tree.c | 2 +- fs/btrfs/fs.h | 1 - 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c index d12c818bfbf1..2fd1e524f54f 100644 --- a/fs/btrfs/disk-io.c +++ b/fs/btrfs/disk-io.c @@ -2905,7 +2905,6 @@ void btrfs_init_fs_info(struct btrfs_fs_info *fs_info) fs_info->nodesize = 4096; fs_info->sectorsize = 4096; fs_info->sectorsize_bits = ilog2(4096); - fs_info->stripesize = 4096; /* Default compress algorithm when user does -o compress */ fs_info->compress_type = BTRFS_COMPRESS_ZLIB; @@ -3355,7 +3354,6 @@ int __cold open_ctree(struct super_block *sb, struct btrfs_fs_devices *fs_device { u32 sectorsize; u32 nodesize; - u32 stripesize; u64 generation; u16 csum_type; struct btrfs_super_block *disk_super; @@ -3464,7 +3462,6 @@ int __cold open_ctree(struct super_block *sb, struct btrfs_fs_devices *fs_device /* Set up fs_info before parsing mount options */ nodesize = btrfs_super_nodesize(disk_super); sectorsize = btrfs_super_sectorsize(disk_super); - stripesize = sectorsize; fs_info->dirty_metadata_batch = nodesize * (1 + ilog2(nr_cpu_ids)); fs_info->delalloc_batch = sectorsize * 512 * (1 + ilog2(nr_cpu_ids)); @@ -3483,7 +3480,6 @@ int __cold open_ctree(struct super_block *sb, struct btrfs_fs_devices *fs_device else fs_info->block_max_order = calc_block_max_order(fs_info->sectorsize_bits); fs_info->csums_per_leaf = BTRFS_MAX_ITEM_SIZE(fs_info) / fs_info->csum_size; - fs_info->stripesize = stripesize; fs_info->fs_devices->fs_info = fs_info; if (fs_info->sectorsize > PAGE_SIZE) diff --git a/fs/btrfs/extent-tree.c b/fs/btrfs/extent-tree.c index 624d76e0ca01..235381b31298 100644 --- a/fs/btrfs/extent-tree.c +++ b/fs/btrfs/extent-tree.c @@ -4757,7 +4757,7 @@ static noinline int find_free_extent(struct btrfs_root *root, /* Checks */ ffe_ctl->search_start = round_up(ffe_ctl->found_offset, - fs_info->stripesize); + fs_info->sectorsize); /* move on to the next group */ if (ffe_ctl->search_start + ffe_ctl->num_bytes > diff --git a/fs/btrfs/fs.h b/fs/btrfs/fs.h index f7f343fbe732..874fb23e4abf 100644 --- a/fs/btrfs/fs.h +++ b/fs/btrfs/fs.h @@ -890,7 +890,6 @@ struct btrfs_fs_info { u32 sectorsize_bits; u32 block_min_order; u32 block_max_order; - u32 stripesize; u32 writeback_bio_size; u32 csum_size; u32 csums_per_leaf; From ba02eab28041f9a4bbe9fc90c7249644fef6de0f Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Tue, 23 Jun 2026 16:56:15 +0100 Subject: [PATCH 18/72] btrfs: defrag: fix deadlock between defrag and delalloc space reservation While running fsstress with autodefrag and flushoncommit, hit a deadlock due to the fact that defrag reserves delalloc space while it's holding dirty and locked folios, besides the extent range lock. The stack traces are the following: [958.624] task:kworker/u50:3 state:D stack:0 pid:20365 tgid:20365 ppid:2 task_flags:0x4208060 flags:0x00080000 [958.626] Workqueue: events_unbound btrfs_async_reclaim_metadata_space [btrfs] [958.627] Call Trace: [958.628] [958.628] __schedule+0x4be/0x10f0 [958.629] ? preempt_count_add+0x69/0xa0 [958.630] schedule+0x26/0xd0 [958.631] wait_current_trans+0x102/0x160 [btrfs] [958.632] ? __pfx_autoremove_wake_function+0x10/0x10 [958.633] start_transaction+0x374/0x900 [btrfs] [958.634] btrfs_commit_current_transaction+0x1d/0x70 [btrfs] [958.635] flush_space+0xca/0x5e0 [btrfs] [958.636] ? _raw_spin_unlock+0x15/0x30 [958.637] ? btrfs_reduce_alloc_profile+0x8c/0x190 [btrfs] [958.639] ? _raw_spin_unlock+0x15/0x30 [958.640] ? calc_available_free_space.isra.0+0x6f/0x110 [btrfs] [958.641] do_async_reclaim_metadata_space+0x84/0x190 [btrfs] [958.642] btrfs_async_reclaim_metadata_space+0x64/0x80 [btrfs] [958.644] process_one_work+0x19d/0x3a0 [958.644] worker_thread+0x1c4/0x330 [958.645] ? __pfx_worker_thread+0x10/0x10 [958.646] kthread+0xfc/0x130 [958.647] ? __pfx_kthread+0x10/0x10 [958.648] ret_from_fork+0x1f7/0x2c0 [958.648] ? __pfx_kthread+0x10/0x10 [958.649] ret_from_fork_asm+0x1a/0x30 [958.650] [958.651] task:kworker/u49:7 state:D stack:0 pid:52990 tgid:52990 ppid:2 task_flags:0x4208060 flags:0x00080000 [958.653] Workqueue: writeback wb_workfn (flush-btrfs-334) [958.655] Call Trace: [958.655] [958.656] __schedule+0x4be/0x10f0 [958.657] ? __blk_flush_plug+0xe9/0x140 [958.658] schedule+0x26/0xd0 [958.658] io_schedule+0x42/0x70 [958.659] folio_wait_bit_common+0x12b/0x330 [958.660] ? folio_wait_bit_common+0x100/0x330 [958.662] ? __pfx_wake_page_function+0x10/0x10 [958.663] extent_write_cache_pages+0x599/0x830 [btrfs] [958.664] ? acpi_fwnode_get_reference_args+0x1fa/0x270 [958.665] btrfs_writepages+0x77/0x130 [btrfs] [958.666] ? __pfx_end_bbio_data_write+0x10/0x10 [btrfs] [958.667] do_writepages+0xc6/0x160 [958.668] __writeback_single_inode+0x42/0x310 [958.669] writeback_sb_inodes+0x231/0x570 [958.670] wb_writeback+0x8a/0x340 [958.671] wb_workfn+0xbf/0x450 [958.672] ? finish_task_switch.isra.0+0xc1/0x350 [958.673] process_one_work+0x19d/0x3a0 [958.673] worker_thread+0x1c4/0x330 [958.674] ? __pfx_worker_thread+0x10/0x10 [958.675] kthread+0xfc/0x130 [958.676] ? __pfx_kthread+0x10/0x10 [958.676] ret_from_fork+0x1f7/0x2c0 [958.677] ? __pfx_kthread+0x10/0x10 [958.678] ret_from_fork_asm+0x1a/0x30 [958.679] [958.679] task:btrfs-cleaner state:D stack:0 pid:296750 tgid:296750 ppid:2 task_flags:0x208040 flags:0x00080000 [958.681] Call Trace: [958.682] [958.682] __schedule+0x4be/0x10f0 [958.683] schedule+0x26/0xd0 [958.684] handle_reserve_ticket+0x1b9/0x2c0 [btrfs] [958.685] ? __pfx_autoremove_wake_function+0x10/0x10 [958.686] reserve_bytes+0x283/0x4c0 [btrfs] [958.687] btrfs_reserve_metadata_bytes+0x18/0xb0 [btrfs] [958.688] btrfs_delalloc_reserve_metadata+0x121/0x320 [btrfs] [958.690] btrfs_delalloc_reserve_space+0x46/0xb0 [btrfs] [958.691] btrfs_defrag_file+0x903/0x1110 [btrfs] [958.692] btrfs_run_defrag_inodes+0x334/0x430 [btrfs] [958.694] cleaner_kthread+0x97/0x1c0 [btrfs] [958.694] ? __pfx_cleaner_kthread+0x10/0x10 [btrfs] [958.696] kthread+0xfc/0x130 [958.696] ? __pfx_kthread+0x10/0x10 [958.697] ret_from_fork+0x1f7/0x2c0 [958.698] ? __pfx_kthread+0x10/0x10 [958.699] ret_from_fork_asm+0x1a/0x30 [958.700] [958.716] task:fsstress state:D stack:0 pid:296769 tgid:296769 ppid:296768 task_flags:0x400140 flags:0x00080000 [958.718] Call Trace: [958.719] [958.719] __schedule+0x4be/0x10f0 [958.720] ? preempt_count_add+0x69/0xa0 [958.721] schedule+0x26/0xd0 [958.722] wb_wait_for_completion+0x79/0xc0 [958.723] ? __pfx_autoremove_wake_function+0x10/0x10 [958.724] __writeback_inodes_sb_nr+0xc5/0xf0 [958.725] try_to_writeback_inodes_sb+0x55/0x70 [958.726] btrfs_commit_transaction+0x19d/0xeb0 [btrfs] [958.727] ? start_transaction+0x343/0x900 [btrfs] [958.728] btrfs_mksubvol+0x28b/0x4e0 [btrfs] [958.729] btrfs_mksnapshot+0x74/0xa0 [btrfs] [958.730] __btrfs_ioctl_snap_create+0x194/0x210 [btrfs] [958.732] btrfs_ioctl_snap_create_v2+0xef/0x150 [btrfs] [958.733] btrfs_ioctl+0x7ec/0x2a70 [btrfs] [958.734] ? __virt_addr_valid+0xe4/0x180 [958.735] ? __check_object_size+0x1cd/0x1f0 [958.736] ? kmem_cache_free+0x146/0x380 [958.737] ? _raw_spin_unlock+0x15/0x30 [958.738] ? do_sys_openat2+0x83/0xd0 [958.739] __x64_sys_ioctl+0x92/0xe0 [958.740] do_syscall_64+0x60/0x590 [958.741] ? clear_bhb_loop+0x60/0xb0 [958.742] entry_SYSCALL_64_after_hwframe+0x76/0x7e [958.743] RIP: 0033:0x7f4431e108db [958.744] RSP: 002b:00007ffcd147db20 EFLAGS: 00000246 ORIG_RAX: 0000000000000010 [958.746] RAX: ffffffffffffffda RBX: 0000000000000004 RCX: 00007f4431e108db [958.747] RDX: 00007ffcd147eb90 RSI: 0000000050009417 RDI: 0000000000000005 [958.749] RBP: 0000000000000000 R08: 0000000000000000 R09: 0000000000000000 [958.751] R10: 0000000000000000 R11: 0000000000000246 R12: 00007ffcd147fbf0 [958.752] R13: 00007ffcd147eb90 R14: 0000000000000005 R15: 0000000000000003 [958.754] What happens is the following: 1) The cleaner kthread is running autodefrag, and in defrag_one_range() it acquired all the folios for the range and locked them. Then it locked the extent range in the inode's iotree. It got two subranges from defrag_collect_targets(), the first one with folio A and the second one with folio B. After it defragged the first subrange, folio A remains locked and dirty - it's only unlocked when defrag_one_range() returns. When it attempts to defrag the second subrange (containing folio B), btrfs_delalloc_reserve_space() creates a space reservation ticket, due to lack of free metadata space and blocks waiting for the async metadata reclaim task to free space and wake it up; 2) The async reclaim metadata task attempts to commit the current transaction, but it blocks because there is another task that started the commit first; 3) A task creating a snapshot is committing the transaction and because the fs was mounted with flushoncommit, it calls try_to_writeback_inodes_sb(), which spawns a task to flush delalloc and waits for it to complete; 4) The task flushing delalloc (kworker/u49:7), finds that folio A for the inode being defragged is dirty, so it tries to lock it... But it blocks because folio A is locked by the defrag task (the cleaner kthread) which is blocked waiting for the reservation ticket to be served, but the async reclaim metadata task is blocked waiting for the transaction commit, which in turn is blocked waiting for the delalloc flush task, which is trying to lock folio A, resulting in a deadlock. The same type of problem can happen if the async reclaim task starts to flush delalloc, as that requires both locking the folio and the extent range in the inode's io tree, and in this case we don't need the fs to be mounted with flushoncommit. This type of problem has ocurred several times in the past with reflinks for example, where we had a dirty folio while holding the extent range locked and then starting a transaction blocked waiting for the async reclaim task due to lack of free metadata space. So fix this by reserving delalloc space before locking folios and locking the extent range in the inode's iotree. We can not simply unlock the folios for each subrange given by defrag_collect_targets() after we defrag it because the same folio may be present too in the next subrange (due to large folios). Fixes: 22b398eeeed4 ("btrfs: defrag: introduce helper to defrag a contiguous prepared range") Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/defrag.c | 50 +++++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/fs/btrfs/defrag.c b/fs/btrfs/defrag.c index f0c6758b7055..0697b285e05f 100644 --- a/fs/btrfs/defrag.c +++ b/fs/btrfs/defrag.c @@ -1130,20 +1130,15 @@ static_assert(PAGE_ALIGNED(CLUSTER_SIZE)); * * - Extent bits are locked */ -static int defrag_one_locked_target(struct btrfs_inode *inode, - struct defrag_target_range *target, - struct folio **folios, int nr_pages, - struct extent_state **cached_state) +static void defrag_one_locked_target(struct btrfs_inode *inode, + struct defrag_target_range *target, + struct folio **folios, int nr_pages, + struct extent_state **cached_state) { struct btrfs_fs_info *fs_info = inode->root->fs_info; - struct extent_changeset *data_reserved = NULL; const u64 start = target->start; const u64 len = target->len; - int ret = 0; - ret = btrfs_delalloc_reserve_space(inode, &data_reserved, start, len); - if (ret < 0) - return ret; btrfs_clear_extent_bit(&inode->io_tree, start, start + len - 1, EXTENT_DELALLOC | EXTENT_DO_ACCOUNTING | EXTENT_DEFRAG, cached_state); @@ -1164,10 +1159,6 @@ static int defrag_one_locked_target(struct btrfs_inode *inode, continue; btrfs_folio_clamp_set_dirty(fs_info, folio, start, len); } - btrfs_delalloc_release_extents(inode, len); - extent_changeset_free(data_reserved); - - return ret; } static int defrag_one_range(struct btrfs_inode *inode, u64 start, u32 len, @@ -1183,6 +1174,8 @@ static int defrag_one_range(struct btrfs_inode *inode, u64 start, u32 len, u64 cur = start; const unsigned int nr_pages = ((start + len - 1) >> PAGE_SHIFT) - (start >> PAGE_SHIFT) + 1; + struct extent_changeset *data_reserved = NULL; + u64 last_defrag_end = start; int ret = 0; ASSERT(nr_pages <= CLUSTER_SIZE / PAGE_SIZE); @@ -1192,6 +1185,22 @@ static int defrag_one_range(struct btrfs_inode *inode, u64 start, u32 len, if (!folios) return -ENOMEM; + /* + * Reserve delalloc space before locking the range and before locking + * and dirtying any folios - otherwise we could deadlock, for example + * after defrag of one range we dirty folios and keep them locked when + * we move to the next range, so reserving delalloc space right before + * each range could trigger flushing of delalloc and deadlock on the + * extent lock or trigger a transaction commit with flushoncommit, which + * can either deadlock on the lock of a folio made dirty in the previous + * range or the extent lock. + */ + ret = btrfs_delalloc_reserve_space(inode, &data_reserved, start, len); + if (ret < 0) { + kfree(folios); + return ret; + } + /* Prepare all pages */ for (int i = 0; cur < start + len && i < nr_pages; i++) { folios[i] = defrag_prepare_one_folio(inode, cur >> PAGE_SHIFT); @@ -1226,10 +1235,11 @@ static int defrag_one_range(struct btrfs_inode *inode, u64 start, u32 len, goto unlock_extent; list_for_each_entry(entry, &target_list, list) { - ret = defrag_one_locked_target(inode, entry, folios, nr_pages, - &cached_state); - if (ret < 0) - break; + defrag_one_locked_target(inode, entry, folios, nr_pages, &cached_state); + if (entry->start > last_defrag_end) + btrfs_delalloc_release_space(inode, data_reserved, last_defrag_end, + entry->start - last_defrag_end, true); + last_defrag_end = entry->start + entry->len; } list_for_each_entry_safe(entry, tmp, &target_list, list) { @@ -1246,6 +1256,12 @@ static int defrag_one_range(struct btrfs_inode *inode, u64 start, u32 len, folio_put(folios[i]); } kfree(folios); + btrfs_delalloc_release_extents(inode, len); + if (last_defrag_end < start + len) + btrfs_delalloc_release_space(inode, data_reserved, last_defrag_end, + start + len - last_defrag_end, true); + extent_changeset_free(data_reserved); + return ret; } From 0429b343f1659a26ff7da8a023fc3ed125481c32 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Tue, 23 Jun 2026 18:13:57 +0100 Subject: [PATCH 19/72] btrfs: fix pending delayed iputs when using autodefrag Syzbot reported the following warning recently: [157.672][ T6611] BTRFS info (device loop0): turning on flush-on-commit [157.672][ T6611] BTRFS info (device loop0): enabling free space tree [157.672][ T6611] BTRFS info (device loop0): enabling auto defrag [157.672][ T6611] BTRFS info (device loop0): use lzo compression, level 1 [157.672][ T6611] BTRFS info (device loop0): max_inline set to 4096 [158.094][ T5608] BTRFS info (device loop2): last unmount of filesystem c9fe44da-de57-406a-8241-57ec7d4412cf [160.073][ T6656] BTRFS info (device loop0 state M): max_inline set to 4096 [160.418][ T5611] BTRFS info (device loop0): last unmount of filesystem ab8108e1-bea5-4a9f-94c9-a3ff208d732a [160.432][ T6662] loop2: detected capacity change from 0 to 32768 [160.438][ T6662] BTRFS: device fsid c9fe44da-de57-406a-8241-57ec7d4412cf devid 1 transid 8 /dev/loop2 (7:2) scanned by syz.2.74 (6662) [160.459][ T6662] BTRFS info (device loop2): first mount of filesystem c9fe44da-de57-406a-8241-57ec7d4412cf [160.459][ T6662] BTRFS info (device loop2): using crc32c checksum algorithm [160.634][ T1187] ------------[ cut here ]------------ [160.634][ T1187] test_bit(BTRFS_FS_STATE_NO_DELAYED_IPUT, &fs_info->fs_state) [160.634][ T1187] WARNING: fs/btrfs/inode.c:3596 at btrfs_add_delayed_iput+0x2e3/0x340, CPU#0: kworker/u8:10/1187 [160.634][ T1187] Modules linked in: [160.634][ T1187] CPU: 0 UID: 0 PID: 1187 Comm: kworker/u8:10 Not tainted syzkaller #0 PREEMPT_{RT,(full)} [160.634][ T1187] Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 04/18/2026 [160.634][ T1187] Workqueue: btrfs-endio-write btrfs_work_helper [160.634][ T1187] RIP: 0010:btrfs_add_delayed_iput+0x2e3/0x340 [160.634][ T1187] Code: 53 a3 45 (...) [160.634][ T1187] RSP: 0018:ffffc900065d77c8 EFLAGS: 00010293 [160.634][ T1187] RAX: ffffffff83e5f502 RBX: ffff88805aba0000 RCX: ffff888029768000 [160.634][ T1187] RDX: 0000000000000000 RSI: 0000000000000000 RDI: 0000000000000000 [160.634][ T1187] RBP: dffffc0000000000 R08: 0000000000000000 R09: 0000000000000000 [160.634][ T1187] R10: dffffc0000000000 R11: ffffed100b574497 R12: 0000000000000001 [160.634][ T1187] R13: dffffc0000000000 R14: ffff888061194788 R15: 0000000000000200 [160.634][ T1187] FS: 0000000000000000(0000) GS:ffff888126186000(0000) knlGS:0000000000000000 [160.634][ T1187] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 [160.634][ T1187] CR2: 00007fe553a3f000 CR3: 00000000596c2000 CR4: 00000000003526f0 [160.634][ T1187] Call Trace: [160.634][ T1187] [160.634][ T1187] btrfs_put_ordered_extent+0x18f/0x430 [160.634][ T1187] btrfs_finish_one_ordered+0xf63/0x2680 [160.634][ T1187] ? __pfx_btrfs_finish_one_ordered+0x10/0x10 [160.634][ T1187] ? do_raw_spin_lock+0x12b/0x2f0 [160.634][ T1187] ? lock_acquire+0x106/0x350 [160.634][ T1187] ? __pfx_do_raw_spin_lock+0x10/0x10 [160.634][ T1187] btrfs_work_helper+0x38b/0xc20 [160.634][ T1187] ? process_scheduled_works+0xa70/0x1860 [160.634][ T1187] process_scheduled_works+0xb5d/0x1860 [160.634][ T1187] ? __pfx_process_scheduled_works+0x10/0x10 [160.634][ T1187] ? assign_work+0x3d5/0x5e0 [160.634][ T1187] worker_thread+0xa53/0xfc0 [160.634][ T1187] kthread+0x388/0x470 [160.634][ T1187] ? __pfx_worker_thread+0x10/0x10 [160.635][ T1187] ? __pfx_kthread+0x10/0x10 [160.635][ T1187] ret_from_fork+0x514/0xb70 [160.635][ T1187] ? __pfx_ret_from_fork+0x10/0x10 [160.635][ T1187] ? __switch_to+0xc79/0x1410 [160.635][ T1187] ? __pfx_kthread+0x10/0x10 [160.635][ T1187] ret_from_fork_asm+0x1a/0x30 [160.635][ T1187] [160.635][ T1187] Kernel panic - not syncing: kernel: panic_on_warn set ... It means we add a delayed iput created after we last ran delayed iputs in close_ctree() and set the flag BTRFS_FS_STATE_NO_DELAYED_IPUT in fs_info. This happens when using autodefrag and more likely to happen if we use flushoncommit too. The steps are the following: 1) Unmount starts, all delalloc is flushed and we enter close_ctree(); 2) In close_ctree() we park the cleaner kthread, but while we wait for it to park, it's in: btrfs_run_defrag_inodes() btrfs_run_defrag_inode() btrfs_defrag_file() defrag_one_cluster() defrag_one_range() defrag_one_locked_target() And dirties some folios from an inode; 3) The cleaner kthread parks and we proceed in close_ctree(), waiting for all ordered extents, running delayed iputs and setting the flag BTRFS_FS_STATE_NO_DELAYED_IPUT in fs_info; 4) Later in close_ctree() we call btrfs_commit_super(), which commits the current transaction. Because we are mounted with flushoncommit, the transaction commit flushes delalloc and waits for the resulting ordered extent to complete; 5) The ordered extents from the flushed delalloc created by autodefrag complete and create delayed iputs, triggering the warning: WARN_ON_ONCE(test_bit(BTRFS_FS_STATE_NO_DELAYED_IPUT, &fs_info->fs_state)); in btrfs_add_delayed_iput() 6) Further below in close_ctree() we will hit the following assertion: ASSERT(list_empty(&fs_info->delayed_iputs)); Since we don't expect any more delayed iputs. Fix this by flushing delalloc and waiting for the ordered extents right after we parked the cleaner kthread and waiting for autodefrag in close_ctree(). Reported-by: syzbot+6a843bf8604711c8fab0@syzkaller.appspotmail.com Link: https://lore.kernel.org/linux-btrfs/6a1ee507.b4221f80.1326c5.0004.GAE@google.com/ Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/disk-io.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c index 2fd1e524f54f..db3f5d3e3e04 100644 --- a/fs/btrfs/disk-io.c +++ b/fs/btrfs/disk-io.c @@ -4377,6 +4377,21 @@ void __cold close_ctree(struct btrfs_fs_info *fs_info) */ flush_workqueue(fs_info->fixup_workers); + /* + * After we entered close_ctree() autodefrag could be running and before + * we parked the cleaner kthread, it dirtied folios of some inode. + * We don't want to leave any delalloc here, it may be flushed any time + * after this point and result in ordered extents that create delayed + * iputs after flushed the ordered extent queues further below, run + * delayed iputs and set BTRFS_FS_STATE_NO_DELAYED_IPUT. If we are + * mounted with flushoncommit, then btrfs_commit_super() called below + * will flush delalloc and wait for ordered extents but we end up + * getting delayed iputs than are never run. So flush delalloc and wait + * for ordered extents. + */ + btrfs_start_delalloc_roots(fs_info, LONG_MAX, false); + btrfs_wait_ordered_roots(fs_info, U64_MAX, NULL); + /* * Handle the error fs first, as it will flush and wait for all ordered * extents. This will generate delayed iputs, thus we want to handle From 28bc34d6476a055ce47d9acbfd2632e3fe18e463 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Wed, 24 Jun 2026 12:31:44 +0100 Subject: [PATCH 20/72] btrfs: defrag: use a single list for each loop in defrag_one_range() There's no need to have one list for each loop to defrag each subrange and then another one to free each subrange (struct defrag_target_range). We can do it in a single loop, freeing each subrange after defragging, plus no need to delete each subrange from the list since we immediately free it. Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/defrag.c | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/fs/btrfs/defrag.c b/fs/btrfs/defrag.c index 0697b285e05f..ad1d04d8f165 100644 --- a/fs/btrfs/defrag.c +++ b/fs/btrfs/defrag.c @@ -1234,16 +1234,12 @@ static int defrag_one_range(struct btrfs_inode *inode, u64 start, u32 len, if (ret < 0) goto unlock_extent; - list_for_each_entry(entry, &target_list, list) { + list_for_each_entry_safe(entry, tmp, &target_list, list) { defrag_one_locked_target(inode, entry, folios, nr_pages, &cached_state); if (entry->start > last_defrag_end) btrfs_delalloc_release_space(inode, data_reserved, last_defrag_end, entry->start - last_defrag_end, true); last_defrag_end = entry->start + entry->len; - } - - list_for_each_entry_safe(entry, tmp, &target_list, list) { - list_del_init(&entry->list); kfree(entry); } unlock_extent: From d5b675c30a287ed0de093919aa69d80d578b3679 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Wed, 24 Jun 2026 18:09:22 +0100 Subject: [PATCH 21/72] btrfs: defrag: use auto kfree in defrag_one_range() for folios array Use AUTO_KFREE() for the folios array, avoiding two kfree() calls, one of them in a very specific error path. Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/defrag.c | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/fs/btrfs/defrag.c b/fs/btrfs/defrag.c index ad1d04d8f165..e454b59d6477 100644 --- a/fs/btrfs/defrag.c +++ b/fs/btrfs/defrag.c @@ -1169,7 +1169,7 @@ static int defrag_one_range(struct btrfs_inode *inode, u64 start, u32 len, struct defrag_target_range *entry; struct defrag_target_range *tmp; LIST_HEAD(target_list); - struct folio **folios; + struct folio AUTO_KFREE(*folios); const u32 sectorsize = inode->root->fs_info->sectorsize; u64 cur = start; const unsigned int nr_pages = ((start + len - 1) >> PAGE_SHIFT) - @@ -1196,10 +1196,8 @@ static int defrag_one_range(struct btrfs_inode *inode, u64 start, u32 len, * range or the extent lock. */ ret = btrfs_delalloc_reserve_space(inode, &data_reserved, start, len); - if (ret < 0) { - kfree(folios); + if (ret < 0) return ret; - } /* Prepare all pages */ for (int i = 0; cur < start + len && i < nr_pages; i++) { @@ -1251,7 +1249,6 @@ static int defrag_one_range(struct btrfs_inode *inode, u64 start, u32 len, folio_unlock(folios[i]); folio_put(folios[i]); } - kfree(folios); btrfs_delalloc_release_extents(inode, len); if (last_defrag_end < start + len) btrfs_delalloc_release_space(inode, data_reserved, last_defrag_end, From 3f66b272ff4f7afad6ab4ed0b011d6cd944346a3 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Wed, 24 Jun 2026 18:13:57 +0100 Subject: [PATCH 22/72] btrfs: defrag: use simple list_del() in defrag_collect_targets() When freeing the entries from the list there is no need to initialize the list member in an entry, since we are immediately freeing it. So use simple list_del() instead of list_del_init(). Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/defrag.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/btrfs/defrag.c b/fs/btrfs/defrag.c index e454b59d6477..7b3f779775a0 100644 --- a/fs/btrfs/defrag.c +++ b/fs/btrfs/defrag.c @@ -1093,7 +1093,7 @@ static int defrag_collect_targets(struct btrfs_inode *inode, struct defrag_target_range *tmp; list_for_each_entry_safe(entry, tmp, target_list, list) { - list_del_init(&entry->list); + list_del(&entry->list); kfree(entry); } } From 217aeb1d50419874b7b50a094a3eb7f64063320a Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Wed, 24 Jun 2026 18:16:25 +0100 Subject: [PATCH 23/72] btrfs: defrag: remove pointless list_del_init() in defrag_one_cluster() There's no need to call list_del_init() against each entry when freeing the list, as the list is local and we are freeing the entry. Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/defrag.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/fs/btrfs/defrag.c b/fs/btrfs/defrag.c index 7b3f779775a0..6ec5dd760d42 100644 --- a/fs/btrfs/defrag.c +++ b/fs/btrfs/defrag.c @@ -1319,10 +1319,8 @@ static int defrag_one_cluster(struct btrfs_inode *inode, inode->root->fs_info->sectorsize_bits; } out: - list_for_each_entry_safe(entry, tmp, &target_list, list) { - list_del_init(&entry->list); + list_for_each_entry_safe(entry, tmp, &target_list, list) kfree(entry); - } if (ret >= 0) *last_scanned_ret = max(*last_scanned_ret, start + len); return ret; From ec78575dde998c21be7e0cb2503b5620f34b6255 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Sat, 27 Jun 2026 09:02:21 +0930 Subject: [PATCH 24/72] btrfs: always wait for ordered extents to avoid OE races [BUG] Syzbot reported a bug that there can be conflicting OEs for the same range: BTRFS critical (device loop4): panic in insert_ordered_extent:264: overlapping ordered extents, existing oe file_offset 16384 num_bytes 430080 flags 0x1089, new oe file_offset 16384 num_bytes 430080 flags 0x80 (errno=-17 Object alrea[ 179.162726][ T6897] BTRFS critical (device loop4): panic in insert_ordered_extent:264: overlapping ordered extents, existing oe file_offset 16384 num_bytes 430080 flags 0x1089, new oe file_offset 16384 num_bytes 430080 flags 0x80 (errno=-17 Object already exists) ------------[ cut here ]------------ kernel BUG at fs/btrfs/ordered-data.c:264! Hardware name: Google Google Compute Engine/Google Compute Engine, BIOS Google 05/09/2026 RIP: 0010:btrfs_alloc_ordered_extent+0x943/0xad0 Call Trace: cow_file_range+0x744/0x12a0 fallback_to_cow+0x5ea/0xa00 run_delalloc_nocow+0x110c/0x17a0 btrfs_run_delalloc_range+0xbe4/0x1c20 writepage_delalloc+0x104d/0x1ba0 btrfs_writepages+0x1667/0x28b0 do_writepages+0x338/0x560 filemap_fdatawrite_range+0x1f2/0x300 btrfs_fdatawrite_range+0x54/0xf0 btrfs_direct_write+0x6a0/0xc30 btrfs_do_write_iter+0x329/0x790 do_iter_readv_writev+0x624/0x8d0 vfs_writev+0x34c/0x990 __se_sys_pwritev2+0x17a/0x2a0 do_syscall_64+0x174/0x580 entry_SYSCALL_64_after_hwframe+0x77/0x7f ---[ end trace 0000000000000000 ]--- [CAUSE] Since commit ff66fe666233 ("btrfs: fix incorrect buffered IO fallback for append direct writes"), if the direct IO finished short, we will revert the isize back to the original one, so that append writes can be respected during the buffered fallback. Normally we rely on lock_and_cleanup_extent_if_need() function during buffered writeback to wait for any existing ordered extents. But that ordered extent waiting only happens if the start_pos is inside the isize. Since we have reverted the isize during failed direct IO, we will not wait for any ordered extents. This means we can have a race where the direct IO OE is still in the tree, finished but not yet removed, then we're inserting the OE for the buffered write, causing the above crash. [FIX] Make the OE wait to be unconditional, to handle the reverted isize situation. And since lock_and_cleanup_extent_if_need() now either lock the extents or return -EAGAIN, also remove the branches that handles no-extent-locked cases, and rename it to remove the "_if_need" suffix. The following micro benchmark shows the runtime difference for btrfs_buffered_write(), doing `xfs_io -f -c "pwrite 0 1m"` workload, all values are the average runtime in nano seconds. function runtime | before | after -----------------------------------+-------------+--------------- lock_and_cleanup_extent_if_need() | 58.2 | 183.0 btrfs_buffered_write() | 2115.6 | 2973.3 The overall runtime of btrfs_buffered_write() is still pretty tiny (still less than 3 micro seconds), I'd say the extra cost is still acceptable. An alternative to fix this problem is to wait ordered extents during iomap_end() where the isize revert is done. But that solution will break nowait requirement, as if a nowait direct IO finished short, we have to wait for the OEs unconditionally or the next append buffered IO can still hit the same problem. So here we have to move the wait cost to buffered write, but at least the code is slightly more streamline. Reported-by: syzbot+ba2afde329fc27e3f22e@syzkaller.appspotmail.com Link: https://syzkaller.appspot.com/bug?extid=ba2afde329fc27e3f22e Fixes: ff66fe666233 ("btrfs: fix incorrect buffered IO fallback for append direct writes") Reviewed-by: Filipe Manana Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/file.c | 104 +++++++++++++++++++----------------------------- 1 file changed, 41 insertions(+), 63 deletions(-) diff --git a/fs/btrfs/file.c b/fs/btrfs/file.c index b46c89771a9f..8f078c58e940 100644 --- a/fs/btrfs/file.c +++ b/fs/btrfs/file.c @@ -875,70 +875,64 @@ static noinline int prepare_one_folio(struct inode *inode, struct folio **folio_ /* * Locks the extent and properly waits for data=ordered extents to finish - * before allowing the folios to be modified if need. + * before allowing the folios to be modified. * * Return: - * 1 - the extent is locked - * 0 - the extent is not locked, and everything is OK + * 0 - the extent is locked * -EAGAIN - need to prepare the folios again */ static noinline int -lock_and_cleanup_extent_if_need(struct btrfs_inode *inode, struct folio *folio, - loff_t pos, size_t write_bytes, - u64 *lockstart, u64 *lockend, bool nowait, - struct extent_state **cached_state) +lock_and_cleanup_extent(struct btrfs_inode *inode, struct folio *folio, + loff_t pos, size_t write_bytes, + u64 *lockstart, u64 *lockend, bool nowait, + struct extent_state **cached_state) { struct btrfs_fs_info *fs_info = inode->root->fs_info; + struct btrfs_ordered_extent *ordered; u64 start_pos; u64 last_pos; - int ret = 0; start_pos = round_down(pos, fs_info->sectorsize); last_pos = round_up(pos + write_bytes, fs_info->sectorsize) - 1; - if (start_pos < inode->vfs_inode.i_size) { - struct btrfs_ordered_extent *ordered; - - if (nowait) { - if (!btrfs_try_lock_extent(&inode->io_tree, start_pos, - last_pos, cached_state)) { - folio_unlock(folio); - folio_put(folio); - return -EAGAIN; - } - } else { - btrfs_lock_extent(&inode->io_tree, start_pos, last_pos, - cached_state); - } - - ordered = btrfs_lookup_ordered_range(inode, start_pos, - last_pos - start_pos + 1); - if (ordered && - ordered->file_offset + ordered->num_bytes > start_pos && - ordered->file_offset <= last_pos) { - btrfs_unlock_extent(&inode->io_tree, start_pos, last_pos, - cached_state); + if (nowait) { + if (!btrfs_try_lock_extent(&inode->io_tree, start_pos, + last_pos, cached_state)) { folio_unlock(folio); folio_put(folio); - btrfs_start_ordered_extent(ordered); - btrfs_put_ordered_extent(ordered); return -EAGAIN; } - if (ordered) - btrfs_put_ordered_extent(ordered); - - *lockstart = start_pos; - *lockend = last_pos; - ret = 1; + } else { + btrfs_lock_extent(&inode->io_tree, start_pos, last_pos, + cached_state); } + ordered = btrfs_lookup_ordered_range(inode, start_pos, + last_pos - start_pos + 1); + if (ordered && + ordered->file_offset + ordered->num_bytes > start_pos && + ordered->file_offset <= last_pos) { + btrfs_unlock_extent(&inode->io_tree, start_pos, last_pos, + cached_state); + folio_unlock(folio); + folio_put(folio); + btrfs_start_ordered_extent(ordered); + btrfs_put_ordered_extent(ordered); + return -EAGAIN; + } + if (ordered) + btrfs_put_ordered_extent(ordered); + + *lockstart = start_pos; + *lockend = last_pos; + /* * We should be called after prepare_one_folio() which should have locked * all pages in the range. */ WARN_ON(!folio_test_locked(folio)); - return ret; + return 0; } /* @@ -1195,7 +1189,6 @@ static int copy_one_range(struct btrfs_inode *inode, struct iov_iter *iter, const u64 reserved_start = round_down(start, fs_info->sectorsize); u64 reserved_len; struct folio *folio = NULL; - int extents_locked; u64 lockstart; u64 lockend; bool only_release_metadata = false; @@ -1253,18 +1246,16 @@ static int copy_one_range(struct btrfs_inode *inode, struct iov_iter *iter, reserved_len = last_block - reserved_start; } - extents_locked = lock_and_cleanup_extent_if_need(inode, folio, start, - write_bytes, &lockstart, - &lockend, nowait, - &cached_state); - if (extents_locked < 0) { - if (!nowait && extents_locked == -EAGAIN) + ret = lock_and_cleanup_extent(inode, folio, start, write_bytes, + &lockstart, &lockend, nowait, &cached_state); + if (ret < 0) { + if (!nowait) goto again; btrfs_delalloc_release_extents(inode, reserved_len); release_space(inode, *data_reserved, reserved_start, reserved_len, only_release_metadata); - return extents_locked; + return ret; } copied = copy_folio_from_iter_atomic(folio, offset_in_folio(folio, start), @@ -1288,11 +1279,8 @@ static int copy_one_range(struct btrfs_inode *inode, struct iov_iter *iter, /* No copied bytes, unlock, release reserved space and exit. */ if (copied == 0) { - if (extents_locked) - btrfs_unlock_extent(&inode->io_tree, lockstart, lockend, - &cached_state); - else - btrfs_free_extent_state(cached_state); + btrfs_unlock_extent(&inode->io_tree, lockstart, lockend, + &cached_state); btrfs_delalloc_release_extents(inode, reserved_len); release_space(inode, *data_reserved, reserved_start, reserved_len, only_release_metadata); @@ -1311,17 +1299,7 @@ static int copy_one_range(struct btrfs_inode *inode, struct iov_iter *iter, ret = btrfs_dirty_folio(inode, folio, start, copied, &cached_state, only_release_metadata); - /* - * If we have not locked the extent range, because the range's start - * offset is >= i_size, we might still have a non-NULL cached extent - * state, acquired while marking the extent range as delalloc through - * btrfs_dirty_page(). Therefore free any possible cached extent state - * to avoid a memory leak. - */ - if (extents_locked) - btrfs_unlock_extent(&inode->io_tree, lockstart, lockend, &cached_state); - else - btrfs_free_extent_state(cached_state); + btrfs_unlock_extent(&inode->io_tree, lockstart, lockend, &cached_state); btrfs_delalloc_release_extents(inode, reserved_len); if (ret) { From e9b7b9d9e78e5beec7f398343c979838ba29645f Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Tue, 16 Jun 2026 17:42:37 +0930 Subject: [PATCH 25/72] btrfs: use IOMAP_DIO_BOUNCE flag instead of falling back to buffered IO Previously btrfs forces direct writes to fall back to buffered ones if the inode has data checksum or the profile has duplication. That fallback is to avoid the content being modified that the final content may mismatch with the checksum or the other mirrors. That brings a pretty huge performance cost, which already caused some concern at that time. But later upstream commit c9d114846b38 ("iomap: add a flag to bounce buffer direct I/O") introduced a new method by copying the content into new pages, and do all the operations based on the newly allocated pages. So let btrfs to utilize the new flag for direct writes if we require stable folios. There is a quick benchmark, using the following fio setup: fio --name=randwrite --filename $mnt/foobar --ioengine=libaio --size=4G \ --rw=randwrite --iodepth=64 --runtime=60 --time_based --direct=1 \ --bs=$blocksize Unit is MiB/s. Blocksize | Zero-copy (*) | Buffered | Bounce -----------+---------------+----------+----------- 4K | 35.1 | 17.1 | 33.8 64K | 522 | 251 | 492 *: This is done by reverting the commit 968f19c5b1b7 ("btrfs: always fallback to buffered write if the inode requires checksum") Although with page bouncing the performance is only around 95% of true-zero copy, it's still almost double the performance of buffered fallback. There will be a small change in behavior, since we're using IOMAP_DIO_BOUNCE flag to allocate new folios, NOWAIT flag will immediately fail. So for true NOWAIT direct IOs, NODATASUM and RAID0/SINGLE profiles are still required. Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/direct-io.c | 58 ++++++++++++++++++++++---------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/fs/btrfs/direct-io.c b/fs/btrfs/direct-io.c index 3e227292b0ac..b80dc45c3ff9 100644 --- a/fs/btrfs/direct-io.c +++ b/fs/btrfs/direct-io.c @@ -817,13 +817,41 @@ static ssize_t btrfs_dio_read(struct kiocb *iocb, struct iov_iter *iter, IOMAP_DIO_PARTIAL | IOMAP_DIO_FSBLOCK_ALIGNED, &data, done_before); } +static bool need_stable_write(struct btrfs_inode *inode) +{ + const u64 data_profile = btrfs_data_alloc_profile(inode->root->fs_info) & + BTRFS_BLOCK_GROUP_PROFILE_MASK; + + /* Data checksum requires stable buffer. */ + if (!(inode->flags & BTRFS_INODE_NODATASUM)) + return true; + /* + * Any profile with mirror/parity will require stable buffer. + * Otherwise the mirror may differ from each other. + * + * Thus only SINGLE and RAID0 doesn't require stable buffer. + */ + if (data_profile != 0 && data_profile != BTRFS_BLOCK_GROUP_RAID0) + return true; + return false; +} + static struct iomap_dio *btrfs_dio_write(struct kiocb *iocb, struct iov_iter *iter, size_t done_before) { struct btrfs_dio_data data = { 0 }; + unsigned int dio_flags = IOMAP_DIO_PARTIAL | IOMAP_DIO_FSBLOCK_ALIGNED; + + if (need_stable_write(BTRFS_I(file_inode(iocb->ki_filp)))) { + /* For now no support for BOUNCE and NOWAIT direct write. */ + if (iocb->ki_flags & IOCB_NOWAIT) + return ERR_PTR(-EAGAIN); + + dio_flags |= IOMAP_DIO_BOUNCE; + } return __iomap_dio_rw(iocb, iter, &btrfs_dio_iomap_ops, &btrfs_dio_ops, - IOMAP_DIO_PARTIAL | IOMAP_DIO_FSBLOCK_ALIGNED, &data, done_before); + dio_flags, &data, done_before); } static ssize_t check_direct_IO(struct btrfs_fs_info *fs_info, @@ -852,8 +880,6 @@ ssize_t btrfs_direct_write(struct kiocb *iocb, struct iov_iter *from) ssize_t ret; unsigned int ilock_flags = 0; struct iomap_dio *dio; - const u64 data_profile = btrfs_data_alloc_profile(fs_info) & - BTRFS_BLOCK_GROUP_PROFILE_MASK; if (iocb->ki_flags & IOCB_NOWAIT) ilock_flags |= BTRFS_ILOCK_TRY; @@ -867,16 +893,6 @@ ssize_t btrfs_direct_write(struct kiocb *iocb, struct iov_iter *from) if (iocb->ki_pos + iov_iter_count(from) <= i_size_read(inode) && IS_NOSEC(inode)) ilock_flags |= BTRFS_ILOCK_SHARED; - /* - * If our data profile has duplication (either extra mirrors or RAID56), - * we can not trust the direct IO buffer, the content may change during - * writeback and cause different contents written to different mirrors. - * - * Thus only RAID0 and SINGLE can go true zero-copy direct IO. - */ - if (data_profile != BTRFS_BLOCK_GROUP_RAID0 && data_profile != 0) - goto buffered; - relock: ret = btrfs_inode_lock(BTRFS_I(inode), ilock_flags); if (ret < 0) @@ -917,22 +933,6 @@ ssize_t btrfs_direct_write(struct kiocb *iocb, struct iov_iter *from) btrfs_inode_unlock(BTRFS_I(inode), ilock_flags); goto buffered; } - /* - * We can't control the folios being passed in, applications can write - * to them while a direct IO write is in progress. This means the - * content might change after we calculated the data checksum. - * Therefore we can end up storing a checksum that doesn't match the - * persisted data. - * - * To be extra safe and avoid false data checksum mismatch, if the - * inode requires data checksum, just fallback to buffered IO. - * For buffered IO we have full control of page cache and can ensure - * no one is modifying the content during writeback. - */ - if (!(BTRFS_I(inode)->flags & BTRFS_INODE_NODATASUM)) { - btrfs_inode_unlock(BTRFS_I(inode), ilock_flags); - goto buffered; - } /* * The iov_iter can be mapped to the same file range we are writing to. From b6890439d41210e5ef9bd620e88ce4f2296506b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miquel=20Sabat=C3=A9=20Sol=C3=A0?= Date: Mon, 29 Jun 2026 23:01:42 +0200 Subject: [PATCH 26/72] btrfs: don't pass tree_id in btrfs_search_path_in_tree() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'tree_id' parameter in btrfs_search_path_in_tree() was only being used in order to fetch the root tree to be considered for the search. For this same reason this function was also requiring a 'struct btrfs_fs_info' parameter. This commit replaces these two parameters with a single 'struct btrfs_root' one, which identifies from which root tree the search should happen. This function only has one caller, the inode lookup ioctl, which knows how to provide the root tree for each case. In fact, if args->treeid == 0, then we don't even have to allocate a new root tree object, and we can reuse the one provided by the ioctl system call, thus avoiding an extra allocation. Signed-off-by: Miquel Sabaté Solà Reviewed-by: Filipe Manana Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/ioctl.c | 49 +++++++++++++++++++++--------------------------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/fs/btrfs/ioctl.c b/fs/btrfs/ioctl.c index 32dd7bbd4d63..a841d5f2830d 100644 --- a/fs/btrfs/ioctl.c +++ b/fs/btrfs/ioctl.c @@ -1664,13 +1664,11 @@ static noinline int btrfs_ioctl_tree_search_v2(struct btrfs_root *root, } /* - * Search INODE_REFs to identify path name of 'dirid' directory - * in a 'tree_id' tree. and sets path name to 'name'. + * Search for an INODE_REF in a 'root' tree which identifies the path name of + * 'dirid'. When found, it sets 'name' with the path name. */ -static noinline int btrfs_search_path_in_tree(struct btrfs_fs_info *info, - u64 tree_id, u64 dirid, char *name) +static noinline int btrfs_search_path_in_tree(struct btrfs_root *root, u64 dirid, char *name) { - struct btrfs_root *root; struct btrfs_key key; char *ptr; int ret = -1; @@ -1692,13 +1690,6 @@ static noinline int btrfs_search_path_in_tree(struct btrfs_fs_info *info, ptr = &name[BTRFS_INO_LOOKUP_PATH_MAX - 1]; - root = btrfs_get_fs_root(info, tree_id, true); - if (IS_ERR(root)) { - ret = PTR_ERR(root); - root = NULL; - goto out; - } - key.objectid = dirid; key.type = BTRFS_INODE_REF_KEY; key.offset = (u64)-1; @@ -1706,11 +1697,9 @@ static noinline int btrfs_search_path_in_tree(struct btrfs_fs_info *info, while (1) { ret = btrfs_search_backwards(root, &key, path); if (ret < 0) - goto out; - else if (ret > 0) { - ret = -ENOENT; - goto out; - } + return ret; + else if (ret > 0) + return -ENOENT; l = path->nodes[0]; slot = path->slots[0]; @@ -1719,10 +1708,8 @@ static noinline int btrfs_search_path_in_tree(struct btrfs_fs_info *info, len = btrfs_inode_ref_name_len(l, iref); ptr -= len + 1; total_len += len + 1; - if (ptr < name) { - ret = -ENAMETOOLONG; - goto out; - } + if (ptr < name) + return -ENAMETOOLONG; *(ptr + len) = '/'; read_extent_buffer(l, ptr, (unsigned long)(iref + 1), len); @@ -1737,10 +1724,8 @@ static noinline int btrfs_search_path_in_tree(struct btrfs_fs_info *info, } memmove(name, ptr, total_len); name[total_len] = '\0'; - ret = 0; -out: - btrfs_put_root(root); - return ret; + + return 0; } static int btrfs_search_path_in_tree_user(struct mnt_idmap *idmap, @@ -1884,6 +1869,7 @@ static int btrfs_search_path_in_tree_user(struct mnt_idmap *idmap, static noinline int btrfs_ioctl_ino_lookup(struct btrfs_root *root, void __user *argp) { + bool new_root = false; struct btrfs_ioctl_ino_lookup_args AUTO_KFREE(args); int ret = 0; @@ -1897,6 +1883,8 @@ static noinline int btrfs_ioctl_ino_lookup(struct btrfs_root *root, */ if (args->treeid == 0) args->treeid = btrfs_root_id(root); + else + new_root = true; if (args->objectid == BTRFS_FIRST_FREE_OBJECTID) { args->name[0] = 0; @@ -1908,9 +1896,14 @@ static noinline int btrfs_ioctl_ino_lookup(struct btrfs_root *root, goto out; } - ret = btrfs_search_path_in_tree(root->fs_info, - args->treeid, args->objectid, - args->name); + if (new_root) { + root = btrfs_get_fs_root(root->fs_info, args->treeid, true); + if (IS_ERR(root)) + return PTR_ERR(root); + } + ret = btrfs_search_path_in_tree(root, args->objectid, args->name); + if (new_root) + btrfs_put_root(root); out: if (ret == 0 && copy_to_user(argp, args, sizeof(*args))) From ce5d6709ebfa4d188d80e149c482c89a28ca21e0 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Fri, 3 Jul 2026 19:16:06 +0930 Subject: [PATCH 27/72] btrfs: add "rescue=usebackuproot" into forced read-only options According to btrfs(5) man page, all rescue options should require a read-only mount. But that read-only check is only introduced for newer rescue options, not for the pre-existing "usebackuproot" one. Furthermore, a filesystem that requires "rescue=" mount option already means it's corrupted, even if "rescue=usebackuproot" allowed the fs to be mounted RW, one should not trust such fs anymore until a comprehensive btrfs-check run and proper evaluation. Change the behavior to match the document, and since "rescue=usebackuproot" is now a full RO mount option, it is no longer a one-shot option, therefore remove it from btrfs_clear_oneshot_options(). Reviewed-by: Johannes Thumshirn Reviewed-by: Neal Gompa Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/fs.h | 3 ++- fs/btrfs/super.c | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/fs/btrfs/fs.h b/fs/btrfs/fs.h index 874fb23e4abf..dcadcf7cc813 100644 --- a/fs/btrfs/fs.h +++ b/fs/btrfs/fs.h @@ -289,7 +289,8 @@ enum { BTRFS_MOUNT_IGNOREBADROOTS | \ BTRFS_MOUNT_IGNOREDATACSUMS | \ BTRFS_MOUNT_IGNOREMETACSUMS | \ - BTRFS_MOUNT_IGNORESUPERFLAGS) + BTRFS_MOUNT_IGNORESUPERFLAGS | \ + BTRFS_MOUNT_USEBACKUPROOT) /* * Compat flags that we support. If any incompat flags are set other than the diff --git a/fs/btrfs/super.c b/fs/btrfs/super.c index 41658705b4e9..11806591e077 100644 --- a/fs/btrfs/super.c +++ b/fs/btrfs/super.c @@ -669,7 +669,6 @@ static int btrfs_parse_param(struct fs_context *fc, struct fs_parameter *param) */ static void btrfs_clear_oneshot_options(struct btrfs_fs_info *fs_info) { - btrfs_clear_opt(fs_info->mount_opt, USEBACKUPROOT); btrfs_clear_opt(fs_info->mount_opt, CLEAR_CACHE); btrfs_clear_opt(fs_info->mount_opt, NOSPACECACHE); } @@ -693,7 +692,8 @@ bool btrfs_check_options(const struct btrfs_fs_info *info, bool ret = true; if (!(flags & SB_RDONLY) && - (check_ro_option(info, *mount_opt, BTRFS_MOUNT_NOLOGREPLAY, "nologreplay") || + (check_ro_option(info, *mount_opt, BTRFS_MOUNT_USEBACKUPROOT, "usebackuproot") || + check_ro_option(info, *mount_opt, BTRFS_MOUNT_NOLOGREPLAY, "nologreplay") || check_ro_option(info, *mount_opt, BTRFS_MOUNT_IGNOREBADROOTS, "ignorebadroots") || check_ro_option(info, *mount_opt, BTRFS_MOUNT_IGNOREDATACSUMS, "ignoredatacsums") || check_ro_option(info, *mount_opt, BTRFS_MOUNT_IGNOREMETACSUMS, "ignoremetacsums") || From 2fd3f6a2711b6986c0fbfa1b3a8e91a778830c10 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Fri, 3 Jul 2026 19:16:07 +0930 Subject: [PATCH 28/72] btrfs: add "rescue=usebackuproot" into "rescue=all" shortcut The mount option "rescue=all" should be a shortcut to include all "rescue=" mount options. But unfortunately "rescue=usebackuproot" is not included. Include that option so "rescue=all" has a better chance to mount a corrupted fs. Reviewed-by: Johannes Thumshirn Reviewed-by: Neal Gompa Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/super.c | 1 + 1 file changed, 1 insertion(+) diff --git a/fs/btrfs/super.c b/fs/btrfs/super.c index 11806591e077..1919f42dfb24 100644 --- a/fs/btrfs/super.c +++ b/fs/btrfs/super.c @@ -621,6 +621,7 @@ static int btrfs_parse_param(struct fs_context *fc, struct fs_parameter *param) btrfs_set_opt(ctx->mount_opt, IGNORESUPERFLAGS); btrfs_set_opt(ctx->mount_opt, IGNOREBADROOTS); btrfs_set_opt(ctx->mount_opt, NOLOGREPLAY); + btrfs_set_opt(ctx->mount_opt, USEBACKUPROOT); break; default: btrfs_info(NULL, "unrecognized rescue option '%s'", From ee2851074b4a555c7f4f883816143cce46880e0c Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Fri, 3 Jul 2026 19:16:08 +0930 Subject: [PATCH 29/72] btrfs: remove "usebackuproot" mount option This mount option is marked deprecated since the introduction of "rescue=" mount option group, in v5.9. That's already a long time ago, and it should be safe to completely remove the old "usebackuproot" mount option now. Reviewed-by: Johannes Thumshirn Reviewed-by: Neal Gompa Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/super.c | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/fs/btrfs/super.c b/fs/btrfs/super.c index 1919f42dfb24..b745794595d1 100644 --- a/fs/btrfs/super.c +++ b/fs/btrfs/super.c @@ -129,7 +129,6 @@ enum { /* Rescue options */ Opt_rescue, - Opt_usebackuproot, /* Debugging options */ Opt_enospc_debug, @@ -249,8 +248,6 @@ static const struct fs_parameter_spec btrfs_fs_parameters[] = { /* Rescue options. */ fsparam_enum("rescue", Opt_rescue, btrfs_parameter_rescue), - /* Deprecated, with alias rescue=usebackuproot */ - __fsparam(NULL, "usebackuproot", Opt_usebackuproot, fs_param_deprecated, NULL), /* For compatibility only, alias for "rescue=nologreplay". */ fsparam_flag("norecovery", Opt_norecovery), @@ -561,14 +558,6 @@ static int btrfs_parse_param(struct fs_context *fc, struct fs_parameter *param) else btrfs_set_opt(ctx->mount_opt, AUTO_DEFRAG); break; - case Opt_usebackuproot: - btrfs_warn(NULL, - "'usebackuproot' is deprecated, use 'rescue=usebackuproot' instead"); - btrfs_set_opt(ctx->mount_opt, USEBACKUPROOT); - - /* If we're loading the backup roots we can't trust the space cache. */ - btrfs_set_opt(ctx->mount_opt, CLEAR_CACHE); - break; case Opt_skip_balance: btrfs_set_opt(ctx->mount_opt, SKIP_BALANCE); break; From 3f950867c307c5413d628a153ac44915bd117ffd Mon Sep 17 00:00:00 2001 From: Shuangpeng Bai Date: Sun, 5 Jul 2026 01:46:35 -0400 Subject: [PATCH 30/72] btrfs: fix extent map leak in NOCOW direct I/O write btrfs_dio_iomap_begin() calls btrfs_get_extent(), which returns an extent map reference that must be dropped on all exit paths. For direct writes into a NOCOW range, btrfs_get_blocks_direct_write() keeps using that extent map and asks btrfs_create_dio_extent() to allocate the ordered extent. If that fails, for example because btrfs_alloc_ordered_extent() fails, the function returns the error without dropping the input extent map. The PREALLOC path avoided this by dropping the input extent map before replacing it with the newly created one. Check the error from btrfs_create_dio_extent() before replacing the map and drop the input extent map on failure. Fixes: 5f9a8a51d8b9 ("Btrfs: add semaphore to synchronize direct IO writes with fsync") CC: stable@vger.kernel.org Reviewed-by: Qu Wenruo Reviewed-by: Filipe Manana Signed-off-by: Shuangpeng Bai Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/direct-io.c | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/fs/btrfs/direct-io.c b/fs/btrfs/direct-io.c index b80dc45c3ff9..27d227f5bdc0 100644 --- a/fs/btrfs/direct-io.c +++ b/fs/btrfs/direct-io.c @@ -280,17 +280,24 @@ static int btrfs_get_blocks_direct_write(struct extent_map **map, em2 = btrfs_create_dio_extent(BTRFS_I(inode), dio_data, start, &file_extent, type); btrfs_dec_nocow_writers(bg); - if (type == BTRFS_ORDERED_PREALLOC) { + if (IS_ERR(em2)) { + ret = PTR_ERR(em2); + btrfs_free_extent_map(em); + *map = NULL; + goto out; + } + + /* + * True NOCOW writes don't need to create a new extent map, + * while PREALLOC writes must replace the existing one. + */ + if (em2) { + ASSERT(type == BTRFS_ORDERED_PREALLOC); btrfs_free_extent_map(em); *map = em2; em = em2; } - if (IS_ERR(em2)) { - ret = PTR_ERR(em2); - goto out; - } - dio_data->nocow_done = true; } else { /* Our caller expects us to free the input extent map. */ From a503aa0610177b0c0098546b68a7ecad69c6448b Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Mon, 6 Jul 2026 12:41:33 +0100 Subject: [PATCH 31/72] btrfs: get rid of useless label in btrfs_create_dio_extent() There's no point in having a label where under it we do nothing but return a variable. So remove it and directly return where we used to goto. Reviewed-by: Johannes Thumshirn Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/direct-io.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fs/btrfs/direct-io.c b/fs/btrfs/direct-io.c index 27d227f5bdc0..ed1779ccb4de 100644 --- a/fs/btrfs/direct-io.c +++ b/fs/btrfs/direct-io.c @@ -150,7 +150,7 @@ static struct extent_map *btrfs_create_dio_extent(struct btrfs_inode *inode, if (type != BTRFS_ORDERED_NOCOW) { em = btrfs_create_io_em(inode, start, file_extent, type); if (IS_ERR(em)) - goto out; + return em; } ordered = btrfs_alloc_ordered_extent(inode, start, file_extent, @@ -167,7 +167,6 @@ static struct extent_map *btrfs_create_dio_extent(struct btrfs_inode *inode, ASSERT(!dio_data->ordered); dio_data->ordered = ordered; } - out: return em; } From ecfe11e8f40bbe734bcc6bfc4fdd19602bed5a6b Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Sun, 5 Jul 2026 21:24:46 +0930 Subject: [PATCH 32/72] btrfs: remove SCRUB_MAX_SECTORS_PER_BLOCK The last user of this macro is removed in commit 001e3fc263ce ("btrfs: scrub: remove scrub_block and scrub_sector structures"). Now that macro is only utilized in an ASSERT(), which no longer makes much sense. Just remove it completely. Reviewed-by: Johannes Thumshirn Signed-off-by: Qu Wenruo Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/scrub.c | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/fs/btrfs/scrub.c b/fs/btrfs/scrub.c index d2f7ac5b6e96..5ec04c91e12d 100644 --- a/fs/btrfs/scrub.c +++ b/fs/btrfs/scrub.c @@ -57,12 +57,6 @@ struct scrub_ctx; #define SCRUB_TOTAL_STRIPES (SCRUB_GROUPS_PER_SCTX * SCRUB_STRIPES_PER_GROUP) -/* - * The following value times PAGE_SIZE needs to be large enough to match the - * largest node/leaf/sector size that shall be supported. - */ -#define SCRUB_MAX_SECTORS_PER_BLOCK (BTRFS_MAX_METADATA_BLOCKSIZE / SZ_4K) - /* Represent one sector and its needed info to verify the content. */ struct scrub_sector_verification { union { @@ -3091,14 +3085,6 @@ int btrfs_scrub_dev(struct btrfs_fs_info *fs_info, u64 devid, u64 start, /* At mount time we have ensured nodesize is in the range of [4K, 64K]. */ ASSERT(fs_info->nodesize <= BTRFS_STRIPE_LEN); - /* - * SCRUB_MAX_SECTORS_PER_BLOCK is calculated using the largest possible - * value (max nodesize / min sectorsize), thus nodesize should always - * be fine. - */ - ASSERT(fs_info->nodesize <= - SCRUB_MAX_SECTORS_PER_BLOCK << fs_info->sectorsize_bits); - /* Allocate outside of device_list_mutex */ sctx = scrub_setup_ctx(fs_info, is_dev_replace); if (IS_ERR(sctx)) From 1462637d55eb25dd0cb6bc35a2c14bb1d5c635e1 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Sun, 5 Jul 2026 21:24:47 +0930 Subject: [PATCH 33/72] btrfs: factor out common scrub read endio into a helper For both scrub_repair_read_endio() and scrub_read_endio(), they share the same bitmap update and bio put. Factor out the common code into a helper to reduce duplication. Reviewed-by: Johannes Thumshirn Signed-off-by: Qu Wenruo Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/scrub.c | 52 ++++++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/fs/btrfs/scrub.c b/fs/btrfs/scrub.c index 5ec04c91e12d..f14955c65821 100644 --- a/fs/btrfs/scrub.c +++ b/fs/btrfs/scrub.c @@ -876,6 +876,30 @@ static int calc_sector_number(struct scrub_stripe *stripe, struct bio_vec *first return i; } +/* + * Common handling of read endio. + * + * The bbio will be released, so no more access to @bbio after this function. + */ +static void scrub_read_endio_common(struct btrfs_bio *bbio) +{ + struct scrub_stripe *stripe = bbio->private; + struct btrfs_fs_info *fs_info = stripe->bg->fs_info; + int sector_nr = calc_sector_number(stripe, bio_first_bvec_all(&bbio->bio)); + const u32 bio_size = bio_get_size(&bbio->bio); + const u32 sectors = bio_size >> fs_info->sectorsize_bits; + + ASSERT(sector_nr < stripe->nr_sectors); + + if (bbio->bio.bi_status) { + scrub_bitmap_set_io_error(stripe, sector_nr, sectors); + scrub_bitmap_set_error(stripe, sector_nr, sectors); + } else { + scrub_bitmap_clear_io_error(stripe, sector_nr, sectors); + } + bio_put(&bbio->bio); +} + /* * Repair read is different to the regular read: * @@ -885,22 +909,9 @@ static int calc_sector_number(struct scrub_stripe *stripe, struct bio_vec *first static void scrub_repair_read_endio(struct btrfs_bio *bbio) { struct scrub_stripe *stripe = bbio->private; - struct btrfs_fs_info *fs_info = stripe->bg->fs_info; - int sector_nr = calc_sector_number(stripe, bio_first_bvec_all(&bbio->bio)); - const u32 bio_size = bio_get_size(&bbio->bio); - ASSERT(sector_nr < stripe->nr_sectors); + scrub_read_endio_common(bbio); - if (bbio->bio.bi_status) { - scrub_bitmap_set_io_error(stripe, sector_nr, - bio_size >> fs_info->sectorsize_bits); - scrub_bitmap_set_error(stripe, sector_nr, - bio_size >> fs_info->sectorsize_bits); - } else { - scrub_bitmap_clear_io_error(stripe, sector_nr, - bio_size >> fs_info->sectorsize_bits); - } - bio_put(&bbio->bio); if (atomic_dec_and_test(&stripe->pending_io)) wake_up(&stripe->io_wait); } @@ -1239,20 +1250,9 @@ static void scrub_stripe_read_repair_worker(struct work_struct *work) static void scrub_read_endio(struct btrfs_bio *bbio) { struct scrub_stripe *stripe = bbio->private; - int sector_nr = calc_sector_number(stripe, bio_first_bvec_all(&bbio->bio)); - int num_sectors; - const u32 bio_size = bio_get_size(&bbio->bio); - ASSERT(sector_nr < stripe->nr_sectors); - num_sectors = bio_size >> stripe->bg->fs_info->sectorsize_bits; + scrub_read_endio_common(bbio); - if (bbio->bio.bi_status) { - scrub_bitmap_set_io_error(stripe, sector_nr, num_sectors); - scrub_bitmap_set_error(stripe, sector_nr, num_sectors); - } else { - scrub_bitmap_clear_io_error(stripe, sector_nr, num_sectors); - } - bio_put(&bbio->bio); if (atomic_dec_and_test(&stripe->pending_io)) { wake_up(&stripe->io_wait); INIT_WORK(&stripe->work, scrub_stripe_read_repair_worker); From 36c9fddcf8b7a6e3c66d7bc12d5b5eed9b9e54d3 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Sun, 5 Jul 2026 21:24:48 +0930 Subject: [PATCH 34/72] btrfs: scrub: implement calc_sector_number() in a faster way Currently calc_sector_number() is implemented by comparing the first bvec of the bbio against all blocks inside a scrub_stripe. This implementation is a little inefficient, and depends on how the scrub buffer is implemented. One of the reason implementing such complex function is that, we do not save the original bvec_iter inside a write btrfs_bio. Although a read bbio has btrfs_bio::saved_iter to get the original logical bytenr, it's not implemented for write bios. On the other hand, since commit 81cea6cd7041 ("btrfs: remove btrfs_bio::fs_info by extracting it from btrfs_bio::inode"), we always set the btrfs_bio::file_offset as the logical bytenr for scrub, and that member will not be modified during IO. So this means we have a stable way to determine the logical bytenr for a scrub bio, now calc_sector_number() is just as simple as: return (bbio->file_offset - stripe->logical) >> sectorsize_bits; Since we're here, also add an ASSERT() to make sure the bbio is inside the stripe, and change the return type to unsigned int to be extra safe. Reviewed-by: Johannes Thumshirn Signed-off-by: Qu Wenruo Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/scrub.c | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/fs/btrfs/scrub.c b/fs/btrfs/scrub.c index f14955c65821..67076d654973 100644 --- a/fs/btrfs/scrub.c +++ b/fs/btrfs/scrub.c @@ -864,16 +864,19 @@ static void scrub_verify_one_stripe(struct scrub_stripe *stripe, unsigned long b } } -static int calc_sector_number(struct scrub_stripe *stripe, struct bio_vec *first_bvec) +static unsigned int calc_sector_number(const struct btrfs_bio *bbio) { - int i; + const struct scrub_stripe *stripe = bbio->private; + const struct btrfs_fs_info *fs_info = stripe->bg->fs_info; - for (i = 0; i < stripe->nr_sectors; i++) { - if (scrub_stripe_get_kaddr(stripe, i) == bvec_virt(first_bvec)) - break; - } - ASSERT(i < stripe->nr_sectors); - return i; + /* Scrub bbios all have their @file_offset set to the logical bytenr. */ + ASSERT(bbio->file_offset >= stripe->logical && + bbio->file_offset < stripe->logical + (stripe->nr_sectors << + fs_info->sectorsize_bits), + "scrub bio logical=%llu stripe logical=%llu stripe len=%u", + bbio->file_offset, stripe->logical, + stripe->nr_sectors << fs_info->sectorsize_bits); + return (bbio->file_offset - stripe->logical) >> fs_info->sectorsize_bits; } /* @@ -885,12 +888,10 @@ static void scrub_read_endio_common(struct btrfs_bio *bbio) { struct scrub_stripe *stripe = bbio->private; struct btrfs_fs_info *fs_info = stripe->bg->fs_info; - int sector_nr = calc_sector_number(stripe, bio_first_bvec_all(&bbio->bio)); + unsigned int sector_nr = calc_sector_number(bbio); const u32 bio_size = bio_get_size(&bbio->bio); const u32 sectors = bio_size >> fs_info->sectorsize_bits; - ASSERT(sector_nr < stripe->nr_sectors); - if (bbio->bio.bi_status) { scrub_bitmap_set_io_error(stripe, sector_nr, sectors); scrub_bitmap_set_error(stripe, sector_nr, sectors); @@ -1264,7 +1265,7 @@ static void scrub_write_endio(struct btrfs_bio *bbio) { struct scrub_stripe *stripe = bbio->private; struct btrfs_fs_info *fs_info = stripe->bg->fs_info; - int sector_nr = calc_sector_number(stripe, bio_first_bvec_all(&bbio->bio)); + unsigned int sector_nr = calc_sector_number(bbio); const u32 bio_size = bio_get_size(&bbio->bio); if (bbio->bio.bi_status) { From 6de9a91f873c72dfeac153ba6084b1c88e15997c Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Sun, 5 Jul 2026 21:24:49 +0930 Subject: [PATCH 35/72] btrfs: use kvmalloc() for stripe buffer of scrub_stripe Currently we're using scrub_stripe::folios[] to store all contents of a stripe. This means we need all the extra work to handle things like sub-page cases, and also require larger folios to handle bs > ps cases. On the other hand, it's not hard to allocate a 64K large folio to cover the full stripe, getting rid of the cross-page handling. Furthermore, even if that large folio allocation failed, we can still use vmalloc() to allocate a virtually contiguous space and still get rid of cross-page handling. This patch will go with kvmalloc() to allocate 64K of memory for the stripe buffer, thus getting rid of all the complex cross-page handling. The following aspects can be greatly simplified: - Checksum verification for both data and metadata No more per-page iteration, all in one go. - RAID56 data caching Just copy the buffer into the RAID56 pages. - No more kaddr/paddr grabbing For most cases the virtual address is enough for csum calculation and io submission. - Bio assembly There is already the helper bio_add_vmalloc() to queue vmallocated memory into a bio. Although it means we have something else to be concerned about: - Bio assembly If the memory is vmallocated, we need to use bio_add_vmalloc() Otherwise use the existing bio_add_page(). - Read endio For vmallocated memory, we need to call invalidate_kernel_vmap_range(). - Scrub bbio bvec size Since scrub_stripe::buffer is kvmallocated, we also need to enlarge the scrub bbio, to be able to handle the worst case, where all 64KiB is allocated by discontiguous 4K physical pages. Signed-off-by: Qu Wenruo Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/raid56.c | 18 ++---- fs/btrfs/raid56.h | 2 +- fs/btrfs/scrub.c | 151 +++++++++++++++++++--------------------------- 3 files changed, 68 insertions(+), 103 deletions(-) diff --git a/fs/btrfs/raid56.c b/fs/btrfs/raid56.c index ffb654d36391..1ee52a9dcee3 100644 --- a/fs/btrfs/raid56.c +++ b/fs/btrfs/raid56.c @@ -2997,13 +2997,11 @@ void raid56_parity_submit_scrub_rbio(struct btrfs_raid_bio *rbio) * This is due to the fact rbio has its own page management for its cache. */ void raid56_parity_cache_data_folios(struct btrfs_raid_bio *rbio, - struct folio **data_folios, u64 data_logical) + void *vaddr, u64 data_logical) { struct btrfs_fs_info *fs_info = rbio->bioc->fs_info; const u64 offset_in_full_stripe = data_logical - rbio->bioc->full_stripe_logical; - unsigned int findex = 0; - unsigned int foffset = 0; int ret; /* @@ -3026,18 +3024,10 @@ void raid56_parity_cache_data_folios(struct btrfs_raid_bio *rbio, cur_off < offset_in_full_stripe + BTRFS_STRIPE_LEN; cur_off += PAGE_SIZE) { const unsigned int pindex = cur_off >> PAGE_SHIFT; - void *kaddr; - kaddr = kmap_local_page(rbio->stripe_pages[pindex]); - memcpy_from_folio(kaddr, data_folios[findex], foffset, PAGE_SIZE); - kunmap_local(kaddr); - - foffset += PAGE_SIZE; - ASSERT(foffset <= folio_size(data_folios[findex])); - if (foffset == folio_size(data_folios[findex])) { - findex++; - foffset = 0; - } + ASSERT(cur_off - offset_in_full_stripe + PAGE_SIZE <= BTRFS_STRIPE_LEN); + memcpy_to_page(rbio->stripe_pages[pindex], 0, + vaddr + cur_off - offset_in_full_stripe, PAGE_SIZE); } bitmap_set(rbio->stripe_uptodate_bitmap, offset_in_full_stripe >> fs_info->sectorsize_bits, diff --git a/fs/btrfs/raid56.h b/fs/btrfs/raid56.h index 1f463ecf7e41..8542648199f1 100644 --- a/fs/btrfs/raid56.h +++ b/fs/btrfs/raid56.h @@ -283,7 +283,7 @@ struct btrfs_raid_bio *raid56_parity_alloc_scrub_rbio(struct bio *bio, void raid56_parity_submit_scrub_rbio(struct btrfs_raid_bio *rbio); void raid56_parity_cache_data_folios(struct btrfs_raid_bio *rbio, - struct folio **data_folios, u64 data_logical); + void *vaddr, u64 data_logical); int btrfs_alloc_stripe_hash_table(struct btrfs_fs_info *info); void btrfs_free_stripe_hash_table(struct btrfs_fs_info *info); diff --git a/fs/btrfs/scrub.c b/fs/btrfs/scrub.c index 67076d654973..f209e75f0ff5 100644 --- a/fs/btrfs/scrub.c +++ b/fs/btrfs/scrub.c @@ -123,19 +123,17 @@ enum { scrub_bitmap_nr_last, }; -#define SCRUB_STRIPE_MAX_FOLIOS (BTRFS_STRIPE_LEN / PAGE_SIZE) - /* * Represent one contiguous range with a length of BTRFS_STRIPE_LEN. */ struct scrub_stripe { struct scrub_ctx *sctx; struct btrfs_block_group *bg; - - struct folio *folios[SCRUB_STRIPE_MAX_FOLIOS]; struct scrub_sector_verification *sectors; - struct btrfs_device *dev; + + void *buffer; + u64 logical; u64 physical; @@ -221,6 +219,9 @@ struct scrub_ctx { refcount_t refs; }; +static_assert(BTRFS_STRIPE_LEN >= PAGE_SIZE); +static_assert(IS_ALIGNED(BTRFS_STRIPE_LEN, PAGE_SIZE)); + #define scrub_calc_start_bit(stripe, name, block_nr) \ ({ \ unsigned int __start_bit; \ @@ -332,13 +333,10 @@ static void release_scrub_stripe(struct scrub_stripe *stripe) if (!stripe) return; - for (int i = 0; i < SCRUB_STRIPE_MAX_FOLIOS; i++) { - if (stripe->folios[i]) - folio_put(stripe->folios[i]); - stripe->folios[i] = NULL; - } + kvfree(stripe->buffer); kfree(stripe->sectors); kfree(stripe->csums); + stripe->buffer = NULL; stripe->sectors = NULL; stripe->csums = NULL; stripe->sctx = NULL; @@ -348,9 +346,6 @@ static void release_scrub_stripe(struct scrub_stripe *stripe) static int init_scrub_stripe(struct btrfs_fs_info *fs_info, struct scrub_stripe *stripe) { - const u32 min_folio_shift = PAGE_SHIFT + fs_info->block_min_order; - int ret; - memset(stripe, 0, sizeof(*stripe)); stripe->nr_sectors = BTRFS_STRIPE_LEN >> fs_info->sectorsize_bits; @@ -361,11 +356,8 @@ static int init_scrub_stripe(struct btrfs_fs_info *fs_info, atomic_set(&stripe->pending_io, 0); spin_lock_init(&stripe->write_error_lock); - ASSERT(BTRFS_STRIPE_LEN >> min_folio_shift <= SCRUB_STRIPE_MAX_FOLIOS); - ret = btrfs_alloc_folio_array(BTRFS_STRIPE_LEN >> min_folio_shift, - fs_info->block_min_order, stripe->folios, - GFP_NOFS); - if (ret < 0) + stripe->buffer = kvmalloc(BTRFS_STRIPE_LEN, GFP_NOFS); + if (!stripe->buffer) goto error; stripe->sectors = kzalloc_objs(struct scrub_sector_verification, @@ -676,32 +668,18 @@ static int fill_writer_pointer_gap(struct scrub_ctx *sctx, u64 physical) return ret; } -static void *scrub_stripe_get_kaddr(struct scrub_stripe *stripe, int sector_nr) +/* + * Unlike the existing csum which is based on paddr, this version is fully on + * vaddr, so no extra per-page iteration needed. + */ +static void scrub_calc_vaddr_csum(struct btrfs_fs_info *fs_info, + void *vaddr, unsigned int len, u8 *dest) { - struct btrfs_fs_info *fs_info = stripe->bg->fs_info; - const u32 min_folio_shift = PAGE_SHIFT + fs_info->block_min_order; - u32 offset = (sector_nr << fs_info->sectorsize_bits); - const struct folio *folio = stripe->folios[offset >> min_folio_shift]; + struct btrfs_csum_ctx csum; - /* stripe->folios[] is allocated by us and no highmem is allowed. */ - ASSERT(folio); - ASSERT(!folio_test_highmem(folio)); - return folio_address(folio) + offset_in_folio(folio, offset); -} - -static phys_addr_t scrub_stripe_get_paddr(struct scrub_stripe *stripe, int sector_nr) -{ - struct btrfs_fs_info *fs_info = stripe->bg->fs_info; - const u32 min_folio_shift = PAGE_SHIFT + fs_info->block_min_order; - u32 offset = (sector_nr << fs_info->sectorsize_bits); - const struct folio *folio = stripe->folios[offset >> min_folio_shift]; - - /* stripe->folios[] is allocated by us and no highmem is allowed. */ - ASSERT(folio); - ASSERT(!folio_test_highmem(folio)); - /* And the range must be contained inside the folio. */ - ASSERT(offset_in_folio(folio, offset) + fs_info->sectorsize <= folio_size(folio)); - return page_to_phys(folio_page(folio, 0)) + offset_in_folio(folio, offset); + btrfs_csum_init(&csum, fs_info->csum_type); + btrfs_csum_update(&csum, vaddr, len); + btrfs_csum_final(&csum, dest); } static void scrub_verify_one_metadata(struct scrub_stripe *stripe, int sector_nr) @@ -709,19 +687,10 @@ static void scrub_verify_one_metadata(struct scrub_stripe *stripe, int sector_nr struct btrfs_fs_info *fs_info = stripe->bg->fs_info; const u32 sectors_per_tree = fs_info->nodesize >> fs_info->sectorsize_bits; const u64 logical = stripe->logical + (sector_nr << fs_info->sectorsize_bits); - void *first_kaddr = scrub_stripe_get_kaddr(stripe, sector_nr); - struct btrfs_header *header = first_kaddr; - struct btrfs_csum_ctx csum; - u8 on_disk_csum[BTRFS_CSUM_SIZE]; + void *first_vaddr = stripe->buffer + (sector_nr << fs_info->sectorsize_bits); + struct btrfs_header *header = first_vaddr; u8 calculated_csum[BTRFS_CSUM_SIZE]; - /* - * Here we don't have a good way to attach the pages (and subpages) - * to a dummy extent buffer, thus we have to directly grab the members - * from pages. - */ - memcpy(on_disk_csum, header->csum, fs_info->csum_size); - if (logical != btrfs_stack_header_bytenr(header)) { scrub_bitmap_set_meta_error(stripe, sector_nr, sectors_per_tree); scrub_bitmap_set_error(stripe, sector_nr, sectors_per_tree); @@ -753,23 +722,15 @@ static void scrub_verify_one_metadata(struct scrub_stripe *stripe, int sector_nr } /* Now check tree block csum. */ - btrfs_csum_init(&csum, fs_info->csum_type); - btrfs_csum_update(&csum, first_kaddr + BTRFS_CSUM_SIZE, - fs_info->sectorsize - BTRFS_CSUM_SIZE); - - for (int i = sector_nr + 1; i < sector_nr + sectors_per_tree; i++) { - btrfs_csum_update(&csum, scrub_stripe_get_kaddr(stripe, i), - fs_info->sectorsize); - } - - btrfs_csum_final(&csum, calculated_csum); - if (memcmp(calculated_csum, on_disk_csum, fs_info->csum_size) != 0) { + scrub_calc_vaddr_csum(fs_info, first_vaddr + BTRFS_CSUM_SIZE, + fs_info->nodesize - BTRFS_CSUM_SIZE, calculated_csum); + if (memcmp(calculated_csum, header->csum, fs_info->csum_size) != 0) { scrub_bitmap_set_meta_error(stripe, sector_nr, sectors_per_tree); scrub_bitmap_set_error(stripe, sector_nr, sectors_per_tree); btrfs_warn_rl(fs_info, "scrub: tree block %llu mirror %u has bad csum, has " BTRFS_CSUM_FMT " want " BTRFS_CSUM_FMT, logical, stripe->mirror_num, - BTRFS_CSUM_FMT_VALUE(fs_info->csum_size, on_disk_csum), + BTRFS_CSUM_FMT_VALUE(fs_info->csum_size, header->csum), BTRFS_CSUM_FMT_VALUE(fs_info->csum_size, calculated_csum)); return; } @@ -795,9 +756,7 @@ static void scrub_verify_one_sector(struct scrub_stripe *stripe, int sector_nr) struct btrfs_fs_info *fs_info = stripe->bg->fs_info; struct scrub_sector_verification *sector = &stripe->sectors[sector_nr]; const u32 sectors_per_tree = fs_info->nodesize >> fs_info->sectorsize_bits; - phys_addr_t paddr = scrub_stripe_get_paddr(stripe, sector_nr); u8 csum_buf[BTRFS_CSUM_SIZE]; - int ret; ASSERT(sector_nr >= 0 && sector_nr < stripe->nr_sectors); @@ -840,8 +799,10 @@ static void scrub_verify_one_sector(struct scrub_stripe *stripe, int sector_nr) return; } - ret = btrfs_check_block_csum(fs_info, paddr, csum_buf, sector->csum); - if (ret < 0) { + scrub_calc_vaddr_csum(fs_info, + stripe->buffer + (sector_nr << fs_info->sectorsize_bits), + fs_info->sectorsize, csum_buf); + if (memcmp(csum_buf, sector->csum, fs_info->csum_size)) { scrub_bitmap_set_bit_csum_error(stripe, sector_nr); scrub_bitmap_set_bit_error(stripe, sector_nr); } else { @@ -892,6 +853,16 @@ static void scrub_read_endio_common(struct btrfs_bio *bbio) const u32 bio_size = bio_get_size(&bbio->bio); const u32 sectors = bio_size >> fs_info->sectorsize_bits; + + /* + * For vmallocated space, readers need to call invalidate_kernel_vmap_range() + * to manage the coherency between kernel mapping and devie space mapping. + */ + if (is_vmalloc_addr(stripe->buffer)) + invalidate_kernel_vmap_range( + stripe->buffer + (sector_nr << fs_info->sectorsize_bits), + bio_size); + if (bbio->bio.bi_status) { scrub_bitmap_set_io_error(stripe, sector_nr, sectors); scrub_bitmap_set_error(stripe, sector_nr, sectors); @@ -927,30 +898,35 @@ static void scrub_bio_add_sector(struct btrfs_bio *bbio, struct scrub_stripe *st int sector_nr) { struct btrfs_fs_info *fs_info = bbio->inode->root->fs_info; - void *kaddr = scrub_stripe_get_kaddr(stripe, sector_nr); + const u32 offset = sector_nr << fs_info->sectorsize_bits; int ret; - ret = bio_add_page(&bbio->bio, virt_to_page(kaddr), fs_info->sectorsize, - offset_in_page(kaddr)); - /* - * Caller should ensure the bbio has enough size. - * And we cannot use __bio_add_page(), which doesn't do any merge. - * - * Meanwhile for scrub_submit_initial_read() we fully rely on the merge - * to create the minimal amount of bio vectors, for fs block size < page - * size cases. - */ + ASSERT(offset + fs_info->sectorsize <= BTRFS_STRIPE_LEN); + + if (is_vmalloc_addr(stripe->buffer)) { + ret = bio_add_vmalloc(&bbio->bio, stripe->buffer + offset, fs_info->sectorsize); + ASSERT(ret == true); + return; + } + ret = bio_add_page(&bbio->bio, virt_to_page(stripe->buffer + offset), + fs_info->sectorsize, offset_in_page(stripe->buffer + offset)); ASSERT(ret == fs_info->sectorsize); } static struct btrfs_bio *alloc_scrub_bbio(struct btrfs_fs_info *fs_info, - unsigned int nr_vecs, blk_opf_t opf, + blk_opf_t opf, u64 logical, btrfs_bio_end_io_t end_io, void *private) { struct btrfs_bio *bbio; - bbio = btrfs_bio_alloc(nr_vecs, opf, BTRFS_I(fs_info->btree_inode), + /* + * Stripe->buffer is allocated by kvmalloc(), which can be pages at + * different physical addresses, we have to ensure the bbio is large + * enough to contain the full stripe. + */ + bbio = btrfs_bio_alloc(BTRFS_STRIPE_LEN >> PAGE_SHIFT, opf, + BTRFS_I(fs_info->btree_inode), logical, end_io, private); bbio->is_scrub = true; bbio->bio.bi_iter.bi_sector = logical >> SECTOR_SHIFT; @@ -982,7 +958,7 @@ static void scrub_stripe_submit_repair_read(struct scrub_stripe *stripe, } if (!bbio) - bbio = alloc_scrub_bbio(fs_info, stripe->nr_sectors, REQ_OP_READ, + bbio = alloc_scrub_bbio(fs_info, REQ_OP_READ, stripe->logical + (i << fs_info->sectorsize_bits), scrub_repair_read_endio, stripe); @@ -1344,7 +1320,7 @@ static void scrub_write_sectors(struct scrub_ctx *sctx, struct scrub_stripe *str bbio = NULL; } if (!bbio) - bbio = alloc_scrub_bbio(fs_info, stripe->nr_sectors, REQ_OP_WRITE, + bbio = alloc_scrub_bbio(fs_info, REQ_OP_WRITE, stripe->logical + (sector_nr << fs_info->sectorsize_bits), scrub_write_endio, stripe); scrub_bio_add_sector(bbio, stripe, sector_nr); @@ -1839,7 +1815,7 @@ static void scrub_submit_extent_sector_read(struct scrub_stripe *stripe) continue; } - bbio = alloc_scrub_bbio(fs_info, stripe->nr_sectors, REQ_OP_READ, + bbio = alloc_scrub_bbio(fs_info, REQ_OP_READ, logical, scrub_read_endio, stripe); } @@ -1864,7 +1840,6 @@ static void scrub_submit_initial_read(struct scrub_ctx *sctx, { struct btrfs_fs_info *fs_info = sctx->fs_info; struct btrfs_bio *bbio; - const u32 min_folio_shift = PAGE_SHIFT + fs_info->block_min_order; unsigned int nr_sectors = stripe_length(stripe) >> fs_info->sectorsize_bits; int mirror = stripe->mirror_num; @@ -1877,7 +1852,7 @@ static void scrub_submit_initial_read(struct scrub_ctx *sctx, return; } - bbio = alloc_scrub_bbio(fs_info, BTRFS_STRIPE_LEN >> min_folio_shift, REQ_OP_READ, + bbio = alloc_scrub_bbio(fs_info, REQ_OP_READ, stripe->logical, scrub_read_endio, stripe); /* Read the whole range inside the chunk boundary. */ for (unsigned int cur = 0; cur < nr_sectors; cur++) @@ -2133,7 +2108,7 @@ static int scrub_raid56_cached_parity(struct scrub_ctx *sctx, for (int i = 0; i < data_stripes; i++) { struct scrub_stripe *stripe = &sctx->raid56_data_stripes[i]; - raid56_parity_cache_data_folios(rbio, stripe->folios, + raid56_parity_cache_data_folios(rbio, stripe->buffer, full_stripe_start + (i << BTRFS_STRIPE_LEN_SHIFT)); } raid56_parity_submit_scrub_rbio(rbio); From 77038da5a975b01dc8447a01d5d8dd07445ef02c Mon Sep 17 00:00:00 2001 From: David Sterba Date: Tue, 30 Jun 2026 02:09:17 +0200 Subject: [PATCH 36/72] btrfs: sink idmap parameter to __btrfs_ioctl_snap_create() The 'idmap' parameter is derived from 'file' that we also pass to __btrfs_ioctl_snap_create(), assign it inside the function. Reviewed-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/ioctl.c | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/fs/btrfs/ioctl.c b/fs/btrfs/ioctl.c index a841d5f2830d..5cb927c6e53d 100644 --- a/fs/btrfs/ioctl.c +++ b/fs/btrfs/ioctl.c @@ -1143,13 +1143,13 @@ static noinline int btrfs_ioctl_resize(struct file *file, } static noinline int __btrfs_ioctl_snap_create(struct file *file, - struct mnt_idmap *idmap, const char *name, unsigned long fd, bool subvol, bool readonly, struct btrfs_qgroup_inherit *inherit) { int ret; struct qstr qname = QSTR(name); + struct mnt_idmap *idmap = file_mnt_idmap(file); if (!S_ISDIR(file_inode(file)->i_mode)) return -ENOTDIR; @@ -1227,8 +1227,7 @@ static noinline int btrfs_ioctl_snap_create(struct file *file, if (ret < 0) return ret; - return __btrfs_ioctl_snap_create(file, file_mnt_idmap(file), - vol_args->name, vol_args->fd, subvol, + return __btrfs_ioctl_snap_create(file, vol_args->name, vol_args->fd, subvol, false, NULL); } @@ -1271,8 +1270,7 @@ static noinline int btrfs_ioctl_snap_create_v2(struct file *file, return ret; } - return __btrfs_ioctl_snap_create(file, file_mnt_idmap(file), - vol_args->name, vol_args->fd, subvol, + return __btrfs_ioctl_snap_create(file, vol_args->name, vol_args->fd, subvol, readonly, inherit); } From c5ac19b98587cfcb3da96e06e6beddc7ac4212d2 Mon Sep 17 00:00:00 2001 From: Sun YangKai Date: Wed, 24 Jun 2026 12:02:27 +0800 Subject: [PATCH 37/72] btrfs: change block group reclaim_mark to bool The reclaim_mark field in struct btrfs_block_group was a u64 that was incremented when marking block groups for reclaim during sweeping, but the actual counter value was never used - only the zero/non-zero state mattered for determining if a block group needed reclaim. Convert it to a bool to properly reflect its usage and reduce memory footprint by 8 bytes. Update assignments to use true/false instead of increment and zero. Reviewed-by: Boris Burkov Signed-off-by: Sun YangKai Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/block-group.c | 2 +- fs/btrfs/block-group.h | 4 +++- fs/btrfs/space-info.c | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/fs/btrfs/block-group.c b/fs/btrfs/block-group.c index c5bcd3c03d24..076c351632c0 100644 --- a/fs/btrfs/block-group.c +++ b/fs/btrfs/block-group.c @@ -3922,7 +3922,7 @@ int btrfs_update_block_group(struct btrfs_trans_handle *trans, old_val += num_bytes; cache->used = old_val; cache->reserved -= num_bytes; - cache->reclaim_mark = 0; + cache->reclaim_mark = false; space_info->bytes_reserved -= num_bytes; space_info->bytes_used += num_bytes; space_info->disk_used += num_bytes * factor; diff --git a/fs/btrfs/block-group.h b/fs/btrfs/block-group.h index 790c2d467af5..69d56864d4ba 100644 --- a/fs/btrfs/block-group.h +++ b/fs/btrfs/block-group.h @@ -263,6 +263,9 @@ struct btrfs_block_group { enum btrfs_block_group_size_class size_class:8; + /* If set, this blockgroup is not used for allocation between two reclaim sweeps. */ + bool reclaim_mark; + /* * Number of extents in this block group used for swap files. * All accesses protected by the spinlock 'lock'. @@ -281,7 +284,6 @@ struct btrfs_block_group { struct list_head active_bg_list; struct work_struct zone_finish_work; struct extent_buffer *last_eb; - u64 reclaim_mark; }; static inline u64 btrfs_block_group_end(const struct btrfs_block_group *block_group) diff --git a/fs/btrfs/space-info.c b/fs/btrfs/space-info.c index e6641597b321..39a28e1bec8a 100644 --- a/fs/btrfs/space-info.c +++ b/fs/btrfs/space-info.c @@ -2156,7 +2156,7 @@ static bool do_reclaim_sweep(struct btrfs_space_info *space_info, int raid) will_reclaim = true; reclaim = true; } - bg->reclaim_mark++; + bg->reclaim_mark = true; spin_unlock(&bg->lock); if (reclaim) btrfs_mark_bg_to_reclaim(bg); From 108cc873398932af589c295f78c348513b8d70d9 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Tue, 14 Jul 2026 20:47:49 +0930 Subject: [PATCH 38/72] btrfs: fix a lockdep caused by path resolution during device scan [BUG] There is a lockdep report related to device scan: ====================================================== WARNING: possible circular locking dependency detected 7.2.0-20260712.rc2.git0.e3321fa3034d.300.fc44.s390x+debug #1 Not tainted ------------------------------------------------------ (udev-worker)/1653 is trying to acquire lock: 0000006919232220 (&type->i_mutex_dir_key#2){++++}-{3:3}, at: lookup_slow+0x3e/0x70 but task is already holding lock: 00000069238564d8 (&fs_devs->device_list_mutex){+.+.}-{3:3}, at: device_list_add.constprop.0+0x148/0xc60 which lock already depends on the new lock. the existing dependency chain (in reverse order) is: -> #5 (&fs_devs->device_list_mutex){+.+.}-{3:3}: lock_acquire+0x150/0x3f0 __mutex_lock+0xba/0xdc0 mutex_lock_nested+0x32/0x40 write_all_supers+0x7a/0x670 btrfs_sync_log+0xae6/0xdd0 btrfs_sync_file+0x4fa/0x7a0 __s390x_sys_fsync+0x52/0xa0 __do_syscall+0x172/0x750 system_call+0x72/0x90 -> #4 (&fs_info->tree_log_mutex){+.+.}-{3:3}: lock_acquire+0x150/0x3f0 __mutex_lock+0xba/0xdc0 mutex_lock_nested+0x32/0x40 btrfs_sync_log+0xaba/0xdd0 btrfs_sync_file+0x4fa/0x7a0 __s390x_sys_fsync+0x52/0xa0 __do_syscall+0x172/0x750 system_call+0x72/0x90 -> #3 (btrfs_trans_num_extwriters){.+.+}-{0:0}: lock_acquire+0x150/0x3f0 join_transaction+0x108/0x680 start_transaction+0x21a/0x660 btrfs_join_transaction+0x32/0x40 btrfs_dirty_inode+0x52/0xf0 touch_atime+0x90/0xc0 filemap_read+0x446/0x450 vfs_read+0x208/0x370 ksys_read+0x88/0x120 __do_syscall+0x172/0x750 system_call+0x72/0x90 -> #2 (btrfs_trans_num_writers){.+.+}-{0:0}: reacquire_held_locks+0x14c/0x240 __lock_release.isra.0+0xd8/0x380 lock_release+0xf6/0x270 percpu_up_read+0x28/0xf0 __btrfs_end_transaction+0x178/0x1f0 btrfs_dirty_inode+0x82/0xf0 touch_atime+0x90/0xc0 btrfs_file_mmap_prepare+0x8c/0xa0 __mmap_region+0x214/0x780 mmap_region+0x108/0x160 do_mmap+0x402/0x5a0 vm_mmap_pgoff+0x156/0x230 ksys_mmap_pgoff+0x17e/0x220 __s390x_sys_old_mmap+0xa8/0x140 __do_syscall+0x172/0x750 system_call+0x72/0x90 -> #1 (&mm->mmap_lock){++++}-{3:3}: lock_acquire+0x150/0x3f0 __might_fault+0x7a/0xa0 filldir64+0x11c/0x210 offset_readdir+0x92/0x200 iterate_dir+0xcc/0x2d0 __do_sys_getdents64+0x7a/0x130 __do_syscall+0x172/0x750 system_call+0x72/0x90 -> #0 (&type->i_mutex_dir_key#2){++++}-{3:3}: check_prev_add+0x160/0xf40 __lock_acquire+0x12aa/0x15a0 lock_acquire+0x150/0x3f0 down_read+0x5a/0x280 lookup_slow+0x3e/0x70 path_lookupat+0x1f0/0x370 filename_lookup+0xce/0x1f0 kern_path+0x48/0x70 is_same_device+0x146/0x300 device_list_add.constprop.0+0x1be/0xc60 btrfs_scan_one_device+0x13a/0x2f0 btrfs_control_ioctl+0x110/0x1e0 __s390x_sys_ioctl+0xfa/0x130 __do_syscall+0x172/0x750 system_call+0x72/0x90 other info that might help us debug this: Chain exists of: &type->i_mutex_dir_key#2 --> &fs_info->tree_log_mutex --> &fs_devs->device_list_mutex Possible unsafe locking scenario: CPU0 CPU1 ---- ---- lock(&fs_devs->device_list_mutex); lock(&fs_info->tree_log_mutex); lock(&fs_devs->device_list_mutex); rlock(&type->i_mutex_dir_key#2); *** DEADLOCK *** 2 locks held by (udev-worker)/1653: #0: 0000016c727051c8 (uuid_mutex){+.+.}-{3:3}, at: btrfs_control_ioctl+0x102/0x1e0 #1: 00000069238564d8 (&fs_devs->device_list_mutex){+.+.}-{3:3}, at: device_list_add.constprop.0+0x148/0xc60 stack backtrace: CPU: 2 UID: 0 PID: 1653 Comm: (udev-worker) Not tainted 7.2.0-20260712.rc2.git0.e3321fa3034d.300.fc44.s390x+debug #1 PREEMPT Hardware name: IBM 3931 A01 701 (LPAR) Call Trace: [<0000016c70680e3e>] dump_stack_lvl+0xae/0x108 [<0000016c7078aa44>] print_circular_bug+0x1a4/0x230 [<0000016c7078ac5c>] check_noncircular+0x18c/0x1b0 [<0000016c7078c030>] check_prev_add+0x160/0xf40 [<0000016c7078fbaa>] __lock_acquire+0x12aa/0x15a0 [<0000016c7078fff0>] lock_acquire+0x150/0x3f0 [<0000016c7180e2fa>] down_read+0x5a/0x280 [<0000016c70b88dde>] lookup_slow+0x3e/0x70 [<0000016c70b8f5d0>] path_lookupat+0x1f0/0x370 [<0000016c70b900ae>] filename_lookup+0xce/0x1f0 [<0000016c70b90218>] kern_path+0x48/0x70 [<0000016c70f5b4a6>] is_same_device+0x146/0x300 [<0000016c70f67cfe>] device_list_add.constprop.0+0x1be/0xc60 [<0000016c70f688da>] btrfs_scan_one_device+0x13a/0x2f0 [<0000016c70ed9cb0>] btrfs_control_ioctl+0x110/0x1e0 [<0000016c70b97d0a>] __s390x_sys_ioctl+0xfa/0x130 [<0000016c718004d2>] __do_syscall+0x172/0x750 [<0000016c718155d2>] system_call+0x72/0x90 [CAUSE] Btrfs device scan will call is_same_device() with device_list_mutex held. But is_same_device() will call kern_path() which will do path resolution and lock the inode. So device scan has the following lock sequence: mutex_lock(device_list_mutex) from device_list_add() | v inode_lock_shared() from lookup_slow() during kern_path(). Meanwhile another thread is fsyncing, which has the following lock sequence: inode_lock() from btrfs_inode_lock() inside btrfs_direct_write() | v mutex_lock(tree_log_mutex() from btrfs_sync_log(), which is further triggered from iomap_dio_complete()->generic_write_sync()->btrfs_sync_file(). | v mutex_lock(device_list_mutex) from write_all_supers() inside btrfs_sync_log(). So the device scan has a reversed lock sequence, compared to the fsync one, this means we can have the following deadlock: Device scan | Fsync ----------------------------------------+-------------------------------- device_list_mutex locked | | inode locked | try to lock device_list_mutex try to lock inode | [FIX] Instead of a full path lookup, use dev_t to determine if two device paths are pointing to the same block device. Inside kernel dev_t is going to uniquely determine a block device, and the device path lookup is already done by lookup_bdev(), which is done without device_list_mutex held, thus no reversed locking sequence. Reported-by: Christian Borntraeger Link: https://lore.kernel.org/linux-btrfs/5a9d9847-4ae6-43c4-afdc-6e5fa51d6117@linux.ibm.com/ Fixes: 2e8b6bc0ab41 ("btrfs: avoid unnecessary device path update for the same device") Tested-by: Christian Borntraeger Reviewed-by: Filipe Manana Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/volumes.c | 37 +------------------------------------ 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c index 2d132c827913..3d1063bc73f8 100644 --- a/fs/btrfs/volumes.c +++ b/fs/btrfs/volumes.c @@ -740,41 +740,6 @@ const u8 *btrfs_sb_fsid_ptr(const struct btrfs_super_block *sb) return has_metadata_uuid ? sb->metadata_uuid : sb->fsid; } -static bool is_same_device(struct btrfs_device *device, const char *new_path) -{ - struct path old = { .mnt = NULL, .dentry = NULL }; - struct path new = { .mnt = NULL, .dentry = NULL }; - char AUTO_KFREE(old_path); - bool is_same = false; - int ret; - - if (!device->name) - goto out; - - old_path = kzalloc(PATH_MAX, GFP_NOFS); - if (!old_path) - goto out; - - rcu_read_lock(); - ret = strscpy(old_path, rcu_dereference(device->name), PATH_MAX); - rcu_read_unlock(); - if (ret < 0) - goto out; - - ret = kern_path(old_path, LOOKUP_FOLLOW, &old); - if (ret) - goto out; - ret = kern_path(new_path, LOOKUP_FOLLOW, &new); - if (ret) - goto out; - if (path_equal(&old, &new)) - is_same = true; -out: - path_put(&old); - path_put(&new); - return is_same; -} - /* * Add new device to list of registered devices * @@ -895,7 +860,7 @@ static noinline struct btrfs_device *device_list_add(const char *path, MAJOR(path_devt), MINOR(path_devt), current->comm, task_pid_nr(current)); - } else if (!device->name || !is_same_device(device, path)) { + } else if (!device->name || device->devt != path_devt) { const char *old_name; /* From 5376c9db45368eb210b4d71104ac00a59dc8b6e0 Mon Sep 17 00:00:00 2001 From: Boris Burkov Date: Tue, 14 Jul 2026 17:21:02 -0700 Subject: [PATCH 39/72] btrfs: write-protect folios during data writeback commit 095be159f3eb ("btrfs: unify folio dirty flag clearing") replaced the folio_clear_dirty_for_io() call in extent_write_cache_pages() with a plain folio_test_dirty() check. Besides clearing the dirty flag, folio_clear_dirty_for_io() also calls folio_mkclean(), which write-protects the shared mmap PTEs mapping the folio. Note that we still do call folio_clear_dirty_for_io() later in submit_one_sector() when we clear dirty on the last sector of the folio (the only sector for non-subpage cases). But we lost this early call in extent_write_cache_pages(). Without the extra write-protection, a process with the file mmap-ed can modify a sector while it is being used by writeback in a way that expects a stable folio (checksumming, compressing, copying, etc...) without faulting, which manifests as a handful of concrete bugs. 1. For large folios or subpage sectorsize, it is possible to submit a bio which does not cover the whole folio. When this happens, we will have a bio in flight for a folio that we have *not* called folio_clear_dirty_for_io() on. If a task with an existing mmap-ed PTE writes (without faulting..) in this window, it can result in corruptions. If the write arrives while the checksumming or writing itself is underway, this can result in an invalid checksum and later corruption reports on read. If the write arrives after checksumming/writing is done but before the last sector dirty is cleared, then the write is present in page cache but doesn't affect the dirty tracking and will be lost when the folio is fully finished being submitted and the dirty bit is cleared. This results in losing the write even if fsync() is called. 2. For zoned submissions which are done in batch separate from the main extent_writepage() loop, we also risk csum violations for those submissions. Zoned writes are clamped to max_zone_append_size and are not aligned with folios, so a submission can span two folios. The first folio being processed in extent_write_cache_pages() will call extent_write_locked_range() which will submit the partial range of the next folio, while the rest of that folio could still be dirty. So clearing dirty on the submitted sectors doesn't call folio_clear_dirty_for_io() and we have the same issue. Since extent_write_cache_pages() skips these batch submitted folios (they are already marked for writeback from submission by the preceding folio), we must add the extra write protection in lock_delalloc_folios(). 3. For inline extents this will subtly risk losing writes that happen after/while we copy the inline extent but before we clear dirty on the folio. 4. For folios spanning EOF, mmap could tamper with the zeroed bytes past EOF and cause them to be persisted where future faults would improperly see them instead of zeros. 5. Finally, for compressed extents, we risk modifying the folios while we work on compressing them which will result in corrupted compressed data. Specifically, in run_delalloc_compressed() we queue up work to do compress_file_range() in BTRFS_COMPRESSION_CHUNK_SIZE (512K) chunks which will call btrfs_folio_clamp_clear_dirty() on the range. For non-subpage, this will always clear the whole folio, safely. For subpage, we risk a partial clear here as well. In particular, imagine a 2M folio broken up into 512K chunks of work which might start compression work on one chunk before all the chunks compress_file_range() workers have gotten far enough to finish clearing all the dirty bitmaps of the folio and getting to folio_clear_dirty_for_io(). Large folios on the edges of submission ranges are similarly at risk to be only partly cleared. This particular gap was introduced by a second patch in the same series: commit a4ef54dbb576 ("btrfs: make extent_range_clear_dirty_for_io() to handle sector size < page size cases") We cannot simply restore the call to folio_clear_dirty_for_io() because that also drops the dirty flag off the folio which violates invariants introduced for large folios by commit 334509ce9d07 ("btrfs: use dirty flag to check if an ordered extent needs to be truncated") and results in failing to invalidate clean folios past i_size, resulting in deadlocks. Therefore, to fix it, leave the existing semantics w.r.t. the folio's dirty flag (to preserve the correct invalidate behavior) but ensure that the other aspect of folio_clear_dirty_for_io(), folio_mkclean(), is run on the folio when we lock it for writeback. Finally, to help prevent similar regressions in the future, add a debug warning that triggers at the known corruption sites if we have failed to write protect the folio. Assisted-by: LLM (debug, reproduce, research fix, review patch) Fixes: 095be159f3eb ("btrfs: unify folio dirty flag clearing") Fixes: a4ef54dbb576 ("btrfs: make extent_range_clear_dirty_for_io() to handle sector size < page size cases") Reviewed-by: Qu Wenruo Signed-off-by: Boris Burkov Signed-off-by: David Sterba --- fs/btrfs/extent_io.c | 31 +++++++++++++++++++++++++++++++ fs/btrfs/extent_io.h | 5 +++++ fs/btrfs/inode.c | 23 +++++++++++++++++------ 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c index eadd8d205411..80e6aaf72e5a 100644 --- a/fs/btrfs/extent_io.c +++ b/fs/btrfs/extent_io.c @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -299,6 +300,25 @@ static noinline void unlock_delalloc_folio(const struct inode *inode, PAGE_UNLOCK); } +#ifdef CONFIG_BTRFS_DEBUG +/* + * Writeback must write-protect a folio when locking it for IO, before + * anything consumes its data (zeroing, inline copy, compression, + * checksumming). If this fails, then an mmap writer would be able to + * modify the data concurrently while we need it to be stable. + */ +void btrfs_check_folio_write_protected(struct folio *folio) +{ + if (folio_mkclean(folio)) { + const struct btrfs_inode *inode = BTRFS_I(folio->mapping->host); + + DEBUG_WARN("writable mmap PTEs, root %llu ino %llu pos %llu order %u", + btrfs_root_id(inode->root), btrfs_ino(inode), folio_pos(folio), + folio_order(folio)); + } +} +#endif + static noinline int lock_delalloc_folios(struct inode *inode, struct folio *locked_folio, u64 start, u64 end) @@ -332,6 +352,8 @@ static noinline int lock_delalloc_folios(struct inode *inode, folio_unlock(folio); goto out; } + /* Locked for writeback; revoke writable mmap PTEs before using the data. */ + folio_mkclean(folio); range_start = max_t(u64, folio_pos(folio), start); range_len = min_t(u64, folio_next_pos(folio), end + 1) - range_start; btrfs_folio_set_lock(fs_info, folio, range_start, range_len); @@ -1893,6 +1915,13 @@ static noinline_for_stack int extent_writepage_io(struct btrfs_inode *inode, ASSERT(end <= folio_end, "start=%llu len=%u folio_start=%llu folio_size=%zu", start, len, folio_start, folio_size(folio)); + /* + * We are about to checksum and write out the data, so it must not be + * mmap writeable, or we could corrupt the data and end up with invalid + * checksums. + */ + btrfs_check_folio_write_protected(folio); + /* Truncate the submit bitmap to the current range. */ if (start > folio_start) bitmap_clear(bio_ctrl->submit_bitmap, 0, @@ -2703,6 +2732,8 @@ static int extent_write_cache_pages(struct address_space *mapping, continue; } + /* Locked for writeback; revoke writable mmap PTEs before using the data. */ + folio_mkclean(folio); ret = extent_writepage(folio, bio_ctrl); if (ret < 0) { done = true; diff --git a/fs/btrfs/extent_io.h b/fs/btrfs/extent_io.h index 9896e15ddc40..869925337699 100644 --- a/fs/btrfs/extent_io.h +++ b/fs/btrfs/extent_io.h @@ -255,6 +255,11 @@ bool try_release_extent_mapping(struct folio *folio, gfp_t mask); int try_release_extent_buffer(struct folio *folio); int btrfs_read_folio(struct file *file, struct folio *folio); +#ifdef CONFIG_BTRFS_DEBUG +void btrfs_check_folio_write_protected(struct folio *folio); +#else +static inline void btrfs_check_folio_write_protected(struct folio *folio) { } +#endif void extent_write_locked_range(struct inode *inode, const struct folio *locked_folio, u64 start, u64 end, struct writeback_control *wbc, bool pages_dirty); diff --git a/fs/btrfs/inode.c b/fs/btrfs/inode.c index b0693065a0c7..2a66bcb59ecb 100644 --- a/fs/btrfs/inode.c +++ b/fs/btrfs/inode.c @@ -775,19 +775,28 @@ static inline void inode_should_defrag(struct btrfs_inode *inode, static int extent_range_clear_dirty_for_io(struct btrfs_inode *inode, u64 start, u64 end) { + pgoff_t index = start >> PAGE_SHIFT; const pgoff_t end_index = end >> PAGE_SHIFT; struct folio *folio; int ret = 0; - for (pgoff_t index = start >> PAGE_SHIFT; index <= end_index; index++) { + while (index <= end_index) { folio = filemap_get_folio(inode->vfs_inode.i_mapping, index); if (IS_ERR(folio)) { if (!ret) ret = PTR_ERR(folio); + index++; continue; } + /* + * We are about to compress the folio, so it must not be mmap + * writeable or we could corrupt the data as we attempt to + * compress it. + */ + btrfs_check_folio_write_protected(folio); btrfs_folio_clamp_clear_dirty(inode->root->fs_info, folio, start, end + 1 - start); + index = folio_next_index(folio); folio_put(folio); } return ret; @@ -877,11 +886,6 @@ static void compress_file_range(struct btrfs_work *work) inode_should_defrag(inode, start, end, end - start + 1, SZ_16K); - /* - * We need to call clear_page_dirty_for_io on each page in the range. - * Otherwise applications with the file mmap'd can wander in and change - * the page contents while we are compressing them. - */ ret = extent_range_clear_dirty_for_io(inode, start, end); /* @@ -2317,6 +2321,13 @@ static int run_delalloc_inline(struct btrfs_inode *inode, struct folio *locked_f int ret; ASSERT(folio_pos(locked_folio) == 0); + /* + * If an mmap writer could modify the folio while we copy it into an + * inline extent we might see only part of their modification then + * wrongly mark it clean again after copying, losing that write. So the + * folio must be write protected here. + */ + btrfs_check_folio_write_protected(locked_folio); if (btrfs_inode_can_compress(inode) && inode_need_compress(inode, 0, blocksize, true)) { From 690c2accacb1aca91ab8186d15dee56da8723f31 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Sat, 20 Jun 2026 14:37:02 +0930 Subject: [PATCH 40/72] btrfs: make sure EXTENT_BUFFER_READING is cleared under refs_lock [FALSE ALERTS] There is a bug report that the warning inside invalidate_and_check_btree_folios() got triggered during btrfs/298: BTRFS info (device sdd): first mount of filesystem f9bf732a-a19b-44b9-99a7-614ddff168e2 BTRFS info (device sdd): using crc32c checksum algorithm BTRFS error (device sdd): failed to find fsid cb2fdb42-b638-4f2f-badd-4127467ba674 when attempting to open seed devices BTRFS error (device sdd): failed to read chunk tree: -2 ------------[ cut here ]------------ WARNING: disk-io.c:3342 at invalidate_and_check_btree_folios+0x260/0x3c0 [btrfs], CPU#4: mount/125993 CPU: 4 UID: 0 PID: 125993 Comm: mount Tainted: G W OE 7.1.0-rc7-custom+ #1 PREEMPT(full) Hardware name: QEMU KVM Virtual Machine, BIOS edk2-20250812-19.fc42 08/12/2025 Call trace: invalidate_and_check_btree_folios+0x260/0x3c0 [btrfs] (P) open_ctree+0x1f50/0x23b0 [btrfs] btrfs_get_tree+0x89c/0xc48 [btrfs] vfs_get_tree+0x30/0x110 vfs_cmd_create+0x58/0xe8 __arm64_sys_fsconfig+0x39c/0x518 invoke_syscall.constprop.0+0x48/0x120 el0_svc_common.constprop.0+0x40/0xe8 do_el0_svc+0x24/0x38 el0_svc+0x50/0x310 el0t_64_sync_handler+0xa0/0xe8 el0t_64_sync+0x198/0x1a0 ---[ end trace 0000000000000000 ]--- BTRFS warning (device sdd): unable to release extent buffer 365985792 owner 3 gen 17 refs 3 flags 0x5 [CAUSE] In that invalidate_and_check_btree_folios() we wait for the eb to finish its read, then check if it's only held by us and the btree inode. If not, then do a warning as it may be still held, and could cause problems. But there is a small window where the check can lead to false alerts: Thread A (Read endio) | Thread B (Unmount) ----------------------------------+------------------------------------- end_bbio_meta_read() | | The eb has one extra ref held | | by the reader, and has | | EXTENT_BUFFER_READING flag set | invalidate_and_check_btree_folios() | | | |- clear_extent_buffer_reading() | | | | |- wait_on_bit_io(); | | | The EXTENT_BUFFER_READING flag is | | | cleared | | |- if (refcount_read(eb->refs) > 2) | | The eb is held by the read, us | | and btree inode, thus it | | will trigger the warning |- free_extent_buffer() | [FIX] Introduce a helper, free_extent_buffer_clear_reading(). If the new parameter, @clear_reading, is set, we will hold the spinlock at the beginning of free_extent_buffer_clear_reading() to make sure the EXTENT_BUFFER_READING flag is cleared inside the same critical section of decreasing refs. Now free_extent_buffer() will just call free_extent_buffer_clear_reading() with @clear_reading set to false, so no behavior change. But for end_bbio_meta_read(), it will not clear_extent_buffer_reading() directly, but pass @clear_reading as true. Then inside invalidate_and_check_btree_folios(), hold the refs_lock before reading refs. So that we eliminate the race window completely. Reported-by: Su Yue Link: https://lore.kernel.org/linux-btrfs/DC0C775E-13B3-47D9-9AB2-895BB11C029D@suse.com/ Fixes: 83f7e52b7ed1 ("btrfs: warn about extent buffer that can not be released") Reviewed-by: Filipe Manana Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/disk-io.c | 16 ++++++++++++++-- fs/btrfs/extent_io.c | 43 +++++++++++++++++++++++++++++++++---------- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c index db3f5d3e3e04..d30c1d02d994 100644 --- a/fs/btrfs/disk-io.c +++ b/fs/btrfs/disk-io.c @@ -3313,6 +3313,8 @@ static void invalidate_and_check_btree_folios(struct btrfs_fs_info *fs_info) */ rcu_read_lock(); xa_for_each(&fs_info->buffer_tree, index, eb) { + unsigned int refs; + /* Increase the ref so that the eb won't disappear. */ if (!refcount_inc_not_zero(&eb->refs)) continue; @@ -3322,17 +3324,27 @@ static void invalidate_and_check_btree_folios(struct btrfs_fs_info *fs_info) if (test_bit(EXTENT_BUFFER_READING, &eb->bflags)) wait_on_bit_io(&eb->bflags, EXTENT_BUFFER_READING, TASK_UNINTERRUPTIBLE); + /* + * We hold the spinlock to make sure above + * EXTENT_BUFFER_READING flag is cleared with the held + * ref dropped. + * Or we can hit a race window and lead to false alerts. + */ + spin_lock(&eb->refs_lock); + refs = refcount_read(&eb->refs); + spin_unlock(&eb->refs_lock); + /* * The refs threshold is 2, one held by us at the beginning * of the loop, one for the ownership in the buffer tree. */ - if (unlikely(refcount_read(&eb->refs) > 2 || extent_buffer_under_io(eb))) { + if (unlikely(refs > 2 || extent_buffer_under_io(eb))) { WARN_ON_ONCE(IS_ENABLED(CONFIG_BTRFS_DEBUG)); btrfs_warn(fs_info, "unable to release extent buffer %llu owner %llu gen %llu refs %u flags 0x%lx", eb->start, btrfs_header_owner(eb), btrfs_header_generation(eb), - refcount_read(&eb->refs), eb->bflags); + refs, eb->bflags); } free_extent_buffer(eb); rcu_read_lock(); diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c index 80e6aaf72e5a..6a516cd1c18b 100644 --- a/fs/btrfs/extent_io.c +++ b/fs/btrfs/extent_io.c @@ -3853,12 +3853,31 @@ static int release_extent_buffer(struct extent_buffer *eb) return 0; } -void free_extent_buffer(struct extent_buffer *eb) +static void clear_extent_buffer_reading(struct extent_buffer *eb) +{ + clear_and_wake_up_bit(EXTENT_BUFFER_READING, &eb->bflags); +} + +static void free_extent_buffer_clear_reading(struct extent_buffer *eb, + bool clear_reading) { int refs; + if (!eb) return; + /* + * We want to clear EXTENT_BUFFER_READING flag and decrease refs + * in the same critical section. + * This will make sure invalidate_and_check_btree_folios() won't + * see an eb with EXTENT_BUFFER_READING cleared but refs not yet + * decreased. + */ + if (clear_reading) { + spin_lock(&eb->refs_lock); + clear_extent_buffer_reading(eb); + } + refs = refcount_read(&eb->refs); while (1) { if (test_bit(EXTENT_BUFFER_UNMAPPED, &eb->bflags)) { @@ -3869,11 +3888,16 @@ void free_extent_buffer(struct extent_buffer *eb) } /* Optimization to avoid locking eb->refs_lock. */ - if (atomic_try_cmpxchg(&eb->refs.refs, &refs, refs - 1)) + if (atomic_try_cmpxchg(&eb->refs.refs, &refs, refs - 1)) { + if (clear_reading) + spin_unlock(&eb->refs_lock); return; + } } - spin_lock(&eb->refs_lock); + if (!clear_reading) + spin_lock(&eb->refs_lock); + if (refcount_read(&eb->refs) == 2 && test_bit(EXTENT_BUFFER_STALE, &eb->bflags) && !extent_buffer_under_io(eb) && @@ -3887,6 +3911,11 @@ void free_extent_buffer(struct extent_buffer *eb) release_extent_buffer(eb); } +void free_extent_buffer(struct extent_buffer *eb) +{ + return free_extent_buffer_clear_reading(eb, false); +} + void free_extent_buffer_stale(struct extent_buffer *eb) { if (!eb) @@ -4012,11 +4041,6 @@ void set_extent_buffer_uptodate(struct extent_buffer *eb) btrfs_meta_folio_set_uptodate(eb->folios[i], eb); } -static void clear_extent_buffer_reading(struct extent_buffer *eb) -{ - clear_and_wake_up_bit(EXTENT_BUFFER_READING, &eb->bflags); -} - static void end_bbio_meta_read(struct btrfs_bio *bbio) { struct extent_buffer *eb = bbio->private; @@ -4040,8 +4064,7 @@ static void end_bbio_meta_read(struct btrfs_bio *bbio) else clear_extent_buffer_uptodate(eb); - clear_extent_buffer_reading(eb); - free_extent_buffer(eb); + free_extent_buffer_clear_reading(eb, true); bio_put(&bbio->bio); } From 4cbafa2ff3510cd64a83b62d3bec9f785cfbb695 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Fri, 22 May 2026 18:53:51 +0930 Subject: [PATCH 41/72] btrfs: remove btrfs_chunk_map::io_(align|width) members Those two members are read from on-disk metadata, but never utilized. And for new chunks we always set those members to BTRFS_STRIPE_LEN anyway. Thus there is no need to keep them inside btrfs_chunk_map. Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/volumes.c | 4 ---- fs/btrfs/volumes.h | 2 -- 2 files changed, 6 deletions(-) diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c index 3d1063bc73f8..b68ede97e8c8 100644 --- a/fs/btrfs/volumes.c +++ b/fs/btrfs/volumes.c @@ -6033,8 +6033,6 @@ static struct btrfs_block_group *create_chunk(struct btrfs_trans_handle *trans, map->chunk_len = ctl->chunk_size; map->stripe_size = ctl->stripe_size; map->type = type; - map->io_align = BTRFS_STRIPE_LEN; - map->io_width = BTRFS_STRIPE_LEN; map->sub_stripes = ctl->sub_stripes; map->num_stripes = ctl->num_stripes; @@ -7597,8 +7595,6 @@ static int read_one_chunk(struct btrfs_key *key, struct extent_buffer *leaf, map->start = logical; map->chunk_len = length; map->num_stripes = num_stripes; - map->io_width = btrfs_chunk_io_width(leaf, chunk); - map->io_align = btrfs_chunk_io_align(leaf, chunk); map->type = type; /* * We can't use the sub_stripes value, as for profiles other than diff --git a/fs/btrfs/volumes.h b/fs/btrfs/volumes.h index 63be45c3298c..30597e1fd240 100644 --- a/fs/btrfs/volumes.h +++ b/fs/btrfs/volumes.h @@ -633,8 +633,6 @@ struct btrfs_chunk_map { u64 chunk_len; u64 stripe_size; u64 type; - int io_align; - int io_width; int num_stripes; int sub_stripes; struct btrfs_io_stripe stripes[]; From 211362e627e12d8b7219aada034f7e7e6a558794 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Fri, 22 May 2026 18:53:52 +0930 Subject: [PATCH 42/72] btrfs: remove duplicated block group type assignment In the function fill_dummy_bgs(), bg->flags is assigned twice. Just remove the second assignment. Signed-off-by: Qu Wenruo Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/block-group.c | 1 - 1 file changed, 1 deletion(-) diff --git a/fs/btrfs/block-group.c b/fs/btrfs/block-group.c index 076c351632c0..f7d16097a0cd 100644 --- a/fs/btrfs/block-group.c +++ b/fs/btrfs/block-group.c @@ -2633,7 +2633,6 @@ static int fill_dummy_bgs(struct btrfs_fs_info *fs_info) bg->flags = map->type; bg->cached = BTRFS_CACHE_FINISHED; bg->used = map->chunk_len; - bg->flags = map->type; bg->space_info = btrfs_find_space_info(fs_info, bg->flags); ret = btrfs_add_block_group_cache(bg); /* From 49ba3a3c0d67aa74618a3f8dabea2e2e372102aa Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Fri, 22 May 2026 18:53:53 +0930 Subject: [PATCH 43/72] btrfs: disguise single-data-RAID56 as RAID1/RAID1C3 Recently kernel RAID56 lib is trying to remove the unexpected single-data-RAID56 (2 disks RAID5 or 3 disk RAID5) support, meanwhile btrfs still supports such setup, which means in the long run btrfs has to handle such corner case by ourselves. Thankfully single-data-RAID56 is really RAID1/RAID1C3, since data and P/Q stripes all match each other, rotation also makes no difference. This patch will disguise those single-data-RAID56 chunks as RAID1/RAID1C3 chunks. This is done at two timings: - Chunk read - Chunk allocation This is done by introducing btrfs_chunk_map::on_disk_type member, which stores the type read from the on-disk metadata. Meanwhile btrfs_chunk_map::type is calculated using on_disk_type. For most profiles @type matches @on_disk_type, but for single-data-RAID56, the @type will be RAID1/RAID1C3. This method has a minimal impact on the fs, all other operations like scrub and read-repair, are all based on the chunk map type, so the disguise method will require no extra modification to those call sites. Although there are still some locations that are checking against block_group->flags, e.g. scrub. Those call sites will still get extra limits assuming the bg is RAID56. But it should not cause any extra problem. Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/block-group.c | 2 +- fs/btrfs/volumes.c | 21 ++++++++++++++++++--- fs/btrfs/volumes.h | 8 ++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/fs/btrfs/block-group.c b/fs/btrfs/block-group.c index f7d16097a0cd..830460a40e86 100644 --- a/fs/btrfs/block-group.c +++ b/fs/btrfs/block-group.c @@ -2630,7 +2630,7 @@ static int fill_dummy_bgs(struct btrfs_fs_info *fs_info) /* Fill dummy cache as FULL */ bg->length = map->chunk_len; - bg->flags = map->type; + bg->flags = map->on_disk_type; bg->cached = BTRFS_CACHE_FINISHED; bg->used = map->chunk_len; bg->space_info = btrfs_find_space_info(fs_info, bg->flags); diff --git a/fs/btrfs/volumes.c b/fs/btrfs/volumes.c index b68ede97e8c8..a8e27db8e4bc 100644 --- a/fs/btrfs/volumes.c +++ b/fs/btrfs/volumes.c @@ -6014,6 +6014,19 @@ struct btrfs_chunk_map *btrfs_alloc_chunk_map(int num_stripes, gfp_t gfp) return map; } +static void set_real_chunk_type(struct btrfs_chunk_map *map) +{ + map->type = map->on_disk_type; + if (likely((map->on_disk_type & BTRFS_BLOCK_GROUP_RAID56_MASK) == 0 || + nr_data_stripes(map) > 1)) + return; + if (map->on_disk_type & BTRFS_BLOCK_GROUP_RAID5) + map->type |= BTRFS_BLOCK_GROUP_RAID1; + else + map->type |= BTRFS_BLOCK_GROUP_RAID1C3; + map->type &= ~BTRFS_BLOCK_GROUP_RAID56_MASK; +} + static struct btrfs_block_group *create_chunk(struct btrfs_trans_handle *trans, struct alloc_chunk_ctl *ctl, struct btrfs_device_info *devices_info) @@ -6032,9 +6045,10 @@ static struct btrfs_block_group *create_chunk(struct btrfs_trans_handle *trans, map->start = start; map->chunk_len = ctl->chunk_size; map->stripe_size = ctl->stripe_size; - map->type = type; + map->on_disk_type = type; map->sub_stripes = ctl->sub_stripes; map->num_stripes = ctl->num_stripes; + set_real_chunk_type(map); for (int i = 0; i < ctl->ndevs; i++) { for (int j = 0; j < ctl->dev_stripes; j++) { @@ -6213,7 +6227,7 @@ int btrfs_chunk_alloc_add_chunk_item(struct btrfs_trans_handle *trans, btrfs_set_stack_chunk_length(chunk, bg->length); btrfs_set_stack_chunk_owner(chunk, BTRFS_EXTENT_TREE_OBJECTID); btrfs_set_stack_chunk_stripe_len(chunk, BTRFS_STRIPE_LEN); - btrfs_set_stack_chunk_type(chunk, map->type); + btrfs_set_stack_chunk_type(chunk, map->on_disk_type); btrfs_set_stack_chunk_num_stripes(chunk, map->num_stripes); btrfs_set_stack_chunk_io_align(chunk, BTRFS_STRIPE_LEN); btrfs_set_stack_chunk_io_width(chunk, BTRFS_STRIPE_LEN); @@ -7595,7 +7609,7 @@ static int read_one_chunk(struct btrfs_key *key, struct extent_buffer *leaf, map->start = logical; map->chunk_len = length; map->num_stripes = num_stripes; - map->type = type; + map->on_disk_type = type; /* * We can't use the sub_stripes value, as for profiles other than * RAID10, they may have 0 as sub_stripes for filesystems created by @@ -7606,6 +7620,7 @@ static int read_one_chunk(struct btrfs_key *key, struct extent_buffer *leaf, */ map->sub_stripes = btrfs_raid_array[index].sub_stripes; map->verified_stripes = 0; + set_real_chunk_type(map); if (num_stripes > 0) map->stripe_size = btrfs_calc_stripe_length(map); diff --git a/fs/btrfs/volumes.h b/fs/btrfs/volumes.h index 30597e1fd240..eaf23c0dcbf6 100644 --- a/fs/btrfs/volumes.h +++ b/fs/btrfs/volumes.h @@ -632,7 +632,15 @@ struct btrfs_chunk_map { u64 start; u64 chunk_len; u64 stripe_size; + /* + * The real type that is utilized during logical address mapping. + * + * For most profiles it matches @on_disk_type, but for single-data-RAID56, + * the real type will be set to RAID1/RAID1C3, to avoid unsupported + * operations from raid56 lib. + */ u64 type; + u64 on_disk_type; int num_stripes; int sub_stripes; struct btrfs_io_stripe stripes[]; From f18dbadb52e43b9a279a6f87d3bf668fa7c41236 Mon Sep 17 00:00:00 2001 From: Hongling Zeng Date: Tue, 14 Jul 2026 09:28:50 +0800 Subject: [PATCH 44/72] btrfs: add missing sctx check in cleanup path in btrfs_ioctl_send() Add sctx NULL check in the for loop condition of the sort_clone_roots cleanup path for consistency with the else branch. Reviewed-by: Boris Burkov Signed-off-by: Hongling Zeng Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/send.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/btrfs/send.c b/fs/btrfs/send.c index 02a1450bf710..dca3570168c7 100644 --- a/fs/btrfs/send.c +++ b/fs/btrfs/send.c @@ -8250,7 +8250,7 @@ long btrfs_ioctl_send(struct btrfs_root *send_root, const struct btrfs_ioctl_sen } if (sort_clone_roots) { - for (i = 0; i < sctx->clone_roots_cnt; i++) { + for (i = 0; sctx && i < sctx->clone_roots_cnt; i++) { btrfs_root_dec_send_in_progress( sctx->clone_roots[i].root); btrfs_put_root(sctx->clone_roots[i].root); From 6d8ba4572922e336f0b59a80751b018e1e135164 Mon Sep 17 00:00:00 2001 From: Guanghui Yang <3497809730@qq.com> Date: Sun, 12 Jul 2026 04:22:32 +0000 Subject: [PATCH 45/72] btrfs: drop recovered reloc root refs on recovery failure During relocation recovery, each fs root gets a reference to its relocation root. If loading or adding a later root fails, or if the first transaction commit fails, btrfs_recover_relocation() jumps to out_unset before merge_reloc_roots() and clean_dirty_subvols(). put_reloc_control() drops the list-owned relocation root references, but it does not clear fs_root->reloc_root or drop the references owned by those pointers. Mount cleanup only drops them when BTRFS_FS_ERROR is set, so an error such as -ENOMEM while processing a later root can leave references behind. Keep temporary references to the fs roots associated during recovery. On failure, clear their reloc_root pointers and drop the corresponding references. Once the first transaction commit succeeds, drop only the temporary fs root references and let the normal merge and cleanup paths handle the relocation roots. Fault injection on a pending-relocation image confirmed the cleanup gap. With an injected first-commit failure, 25 fs roots had reloc_root set with fs_error=0. With this fix, the same failure path drops that count to 0 before mount fails. Fixes: f44deb7442ed ("btrfs: hold a ref on the root->reloc_root") CC: stable@vger.kernel.org Signed-off-by: Guanghui Yang <3497809730@qq.com> Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/relocation.c | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/fs/btrfs/relocation.c b/fs/btrfs/relocation.c index f14bb4158d8d..da54db75e7a9 100644 --- a/fs/btrfs/relocation.c +++ b/fs/btrfs/relocation.c @@ -5593,6 +5593,24 @@ static noinline_for_stack int mark_garbage_root(struct btrfs_root *root) return ret; } +static void release_recovered_fs_roots(struct list_head *roots, bool drop_reloc_refs) +{ + struct btrfs_root *root; + struct btrfs_root *next; + + list_for_each_entry_safe(root, next, roots, reloc_dirty_list) { + list_del_init(&root->reloc_dirty_list); + if (drop_reloc_refs) { + struct btrfs_root *reloc_root = root->reloc_root; + + ASSERT(reloc_root); + root->reloc_root = NULL; + btrfs_put_root(reloc_root); + } + btrfs_put_root(root); + } +} + /* * recover relocation interrupted by system crash. * @@ -5602,6 +5620,7 @@ static noinline_for_stack int mark_garbage_root(struct btrfs_root *root) int btrfs_recover_relocation(struct btrfs_fs_info *fs_info) { LIST_HEAD(reloc_roots); + LIST_HEAD(recovered_roots); struct btrfs_key key; struct btrfs_root *fs_root; struct btrfs_root *reloc_root; @@ -5718,7 +5737,7 @@ int btrfs_recover_relocation(struct btrfs_fs_info *fs_info) ret = PTR_ERR(fs_root); list_add_tail(&reloc_root->root_list, &reloc_roots); btrfs_end_transaction(trans); - goto out_unset; + goto out_drop_reloc_refs; } ret = __add_reloc_root(reloc_root, rc); @@ -5727,15 +5746,17 @@ int btrfs_recover_relocation(struct btrfs_fs_info *fs_info) list_add_tail(&reloc_root->root_list, &reloc_roots); btrfs_put_root(fs_root); btrfs_end_transaction(trans); - goto out_unset; + goto out_drop_reloc_refs; } + ASSERT(list_empty(&fs_root->reloc_dirty_list)); fs_root->reloc_root = btrfs_grab_root(reloc_root); - btrfs_put_root(fs_root); + list_add_tail(&fs_root->reloc_dirty_list, &recovered_roots); } ret = btrfs_commit_transaction(trans); if (ret) - goto out_unset; + goto out_drop_reloc_refs; + release_recovered_fs_roots(&recovered_roots, false); ret = merge_reloc_roots(rc); if (ret) @@ -5753,6 +5774,8 @@ int btrfs_recover_relocation(struct btrfs_fs_info *fs_info) ret2 = clean_dirty_subvols(rc); if (ret2 < 0 && !ret) ret = ret2; +out_drop_reloc_refs: + release_recovered_fs_roots(&recovered_roots, true); out_unset: unset_reloc_control(rc); reloc_chunk_end(fs_info); From 01c2f41fc441f75b3f9326443372faceb1cb6638 Mon Sep 17 00:00:00 2001 From: Sun YangKai Date: Thu, 9 Jul 2026 16:23:35 +0800 Subject: [PATCH 46/72] btrfs: check if root is readonly when setting posix acl For a filesystem which has btrfs read-only property set to true, all write operations including acl and xattr should be denied. However, acl can still be set even if btrfs ro property is true. This happens because no function on the set_acl code path checks the root is readonly or not. It was checked in btrfs_setxattr_trans() but got removed in commit 353c2ea735e4 ("btrfs: remove redundant readonly root check in btrfs_setxattr_trans") That commit didn't check if all the callers properly check the root's read-only flag. A previous fix is commit b51111271b03 ("btrfs: check if root is readonly while setting security xattr"). Always check if the root is read-only before performing the set acl operation. Fixes: 353c2ea735e4 ("btrfs: remove redundant readonly root check in btrfs_setxattr_trans") Reviewed-by: Johannes Thumshirn Reviewed-by: Filipe Manana Signed-off-by: Sun YangKai Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/acl.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fs/btrfs/acl.c b/fs/btrfs/acl.c index e55b686fe1ab..662cdd1cbdef 100644 --- a/fs/btrfs/acl.c +++ b/fs/btrfs/acl.c @@ -15,6 +15,7 @@ #include "xattr.h" #include "acl.h" #include "misc.h" +#include "btrfs_inode.h" struct posix_acl *btrfs_get_acl(struct inode *inode, int type, bool rcu) { @@ -107,6 +108,9 @@ int btrfs_set_acl(struct mnt_idmap *idmap, struct dentry *dentry, struct inode *inode = d_inode(dentry); umode_t old_mode = inode->i_mode; + if (btrfs_root_readonly(BTRFS_I(inode)->root)) + return -EROFS; + if (type == ACL_TYPE_ACCESS && acl) { ret = posix_acl_update_mode(idmap, inode, &inode->i_mode, &acl); From f6c02cd048bd7db865a2e2d65cae78aea64444fa Mon Sep 17 00:00:00 2001 From: Rosen Penev Date: Fri, 3 Jul 2026 15:45:57 -0700 Subject: [PATCH 47/72] btrfs: compression: allocate heuristic buckets with workspace Avoid allocating the heuristic buckets separately from the workspace, the lifetime is the same. The new size of struct heuristic_ws is 2112. SLUB merges same/similar sized structures for the named caches, so there's a chance such size already exists on the system, like below: $ grep 2112 /proc/slabinfo sighand_cache 593 1335 2112 15 8 Signed-off-by: Rosen Penev Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/compression.c | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/fs/btrfs/compression.c b/fs/btrfs/compression.c index ffb6b52863a7..c62b5148d5ac 100644 --- a/fs/btrfs/compression.c +++ b/fs/btrfs/compression.c @@ -651,9 +651,9 @@ struct heuristic_ws { u8 *sample; u32 sample_size; /* Buckets store counters for each byte value */ - struct bucket_item *bucket; + struct bucket_item bucket[BUCKET_SIZE]; /* Sorting buffer */ - struct bucket_item *bucket_b; + struct bucket_item bucket_b[BUCKET_SIZE]; struct list_head list; }; @@ -664,8 +664,6 @@ static void free_heuristic_ws(struct list_head *ws) workspace = list_entry(ws, struct heuristic_ws, list); kvfree(workspace->sample); - kfree(workspace->bucket); - kfree(workspace->bucket_b); kfree(workspace); } @@ -681,14 +679,6 @@ static struct list_head *alloc_heuristic_ws(struct btrfs_fs_info *fs_info) if (!ws->sample) goto fail; - ws->bucket = kzalloc_objs(*ws->bucket, BUCKET_SIZE); - if (!ws->bucket) - goto fail; - - ws->bucket_b = kzalloc_objs(*ws->bucket_b, BUCKET_SIZE); - if (!ws->bucket_b) - goto fail; - INIT_LIST_HEAD(&ws->list); return &ws->list; fail: From 67bd829a14b7d71ffea28147b98dadf9ee684568 Mon Sep 17 00:00:00 2001 From: Leo Martins Date: Wed, 1 Jul 2026 16:47:10 -0700 Subject: [PATCH 48/72] btrfs: replace writeback inhibition xarray with a fixed inline buffer Commit f9a48549a15a ("btrfs: inhibit extent buffer writeback to prevent COW amplification") tracks the extent buffers a transaction handle has inhibited in a per-handle xarray. Keying the tracking to the transaction handle is correct, but using an xarray for it causes two problems in production. First, a write_iops regression. Every COW calls btrfs_inhibit_eb_writeback() from btrfs_force_cow_block() and should_cow_block(), which does an xa_store() keyed by eb->start. The kernel test robot reported a 22.6% fio.write_iops regression on a single-task 4k randwrite workload (ftruncate ioengine, buffered IO) on btrfs. The cost is the per-COW xarray store done on every COW'd block. Replacing it with a non-allocating fixed buffer recovers the lost throughput, and that buffer does more per-COW bookkeeping yet still recovers, so the cost is the xarray operation itself rather than the extra tracking work. Second, an unbounded cleanup walk. btrfs_uninhibit_all_eb_writeback() iterates every eb the handle inhibited with xa_for_each(). A single handle that COWs a very large number of blocks (inode eviction, or truncate of a file with many extents, where btrfs_truncate_inode_items() loops over many search_again descents under one handle) makes that walk arbitrarily long. It runs in __btrfs_end_transaction() before num_writers is dropped, so it blocks the committing thread; this shows up as multi-second stalls and RCU stall reports. Replace the xarray with a fixed inline array on btrfs_trans_handle, managed with a CLOCK (second-chance) eviction policy. Inhibiting a buffer becomes an array append with no allocation and no tree walk, and the end-of-handle cleanup is bounded by the array size. The set that actually needs protection is the working set the handle revisits across search_again descents, the search path frontier, which is on the order of the tree height. It is not every block the handle ever COWs. should_cow_block() re-inhibiting an already tracked buffer marks it referenced, so revisited buffers survive eviction while write-once buffers are reclaimed first. A small fixed buffer is therefore enough where a non-evicting array would either overflow or have to grow without bound. BTRFS_INHIBITED_EBS_SLOTS is 8 and the reference bits pack into a u32. The CLOCK eviction is what justifies the extra complexity over a plain non-evicting array. The test workload stresses amplification: it removes 16 heavily fragmented 64 MiB files in one transaction while background writeback keeps writing out in-use metadata. A re-COW event is a buffer already COWed in the running transaction that was written back and then COWed again; the figure below is the ratio of re-COW events to first-COW events summed across the eviction (n=5, lower is better): tracking re-COW per first-COW no inhibition 6.1 non-evicting array, 32 slots 3.8 CLOCK array, 8 slots (this patch) 1.6 unbounded xarray (reverted) 1.4 The non-evicting array fills with write-once buffers and stops covering the buffers the handle keeps revisiting, so even at four times the slots it leaves most of the amplification. CLOCK evicts the cold buffers and keeps the revisited ones, recovering almost all of the unbounded benefit. The eviction policy, not the buffer size, is what closes the gap. eb->writeback_inhibitors and the WB_SYNC_ALL bypass in lock_extent_buffer_for_io() are unchanged, so fsync and commit behavior are unaffected. A reference is taken on each tracked buffer so it cannot be freed while the array points at it; eviction drops that reference and the inhibitor count. There's another testing report, showing 20% latency improvement on reflink and deduplication synthetic benchmark. Full detailed report at https://github.com/lcf0399/linux-regression-evidence/tree/main/btrfs-remap-writeback-inhibition-v2 . Link: https://lore.kernel.org/all/CANGjgd=fQkHht2PdDi-+EAdzWH7UtxxWhhJ7b80Rr17PbpgxOw@mail.gmail.com/ Reported-by: kernel test robot Fixes: f9a48549a15a ("btrfs: inhibit extent buffer writeback to prevent COW amplification") Closes: https://lore.kernel.org/oe-lkp/202603112240.f7605968-lkp@intel.com Tested-by: Chengfeng Lin Reviewed-by: Filipe Manana Reviewed-by: Sun YangKai Signed-off-by: Leo Martins Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/extent_io.c | 91 ++++++++++++++++++++++++++---------------- fs/btrfs/transaction.c | 2 - fs/btrfs/transaction.h | 25 ++++++++++-- 3 files changed, 79 insertions(+), 39 deletions(-) diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c index 6a516cd1c18b..116cbc8a34fa 100644 --- a/fs/btrfs/extent_io.c +++ b/fs/btrfs/extent_io.c @@ -3140,47 +3140,71 @@ static inline void btrfs_release_extent_buffer(struct extent_buffer *eb) kmem_cache_free(extent_buffer_cache, eb); } +/* + * Claim a slot to track an extent buffer in, evicting the coldest tracked buffer + * when the array is full. + * + * Slots fill in order until the array is full. After that a CLOCK (second + * chance) scan advances the hand, clearing one reference bit per step, until + * it lands on an unreferenced slot whose buffer is evicted. Clearing a bit per + * step bounds the scan to BTRFS_INHIBITED_EBS_SLOTS iterations. + */ +static int btrfs_inhibit_claim_slot(struct btrfs_trans_handle *trans) +{ + int slot; + + if (trans->nr_inhibited_ebs < BTRFS_INHIBITED_EBS_SLOTS) + return trans->nr_inhibited_ebs++; + + while (trans->inhibited_ebs_referenced & (1U << trans->inhibited_ebs_hand)) { + trans->inhibited_ebs_referenced &= ~(1U << trans->inhibited_ebs_hand); + trans->inhibited_ebs_hand = + (trans->inhibited_ebs_hand + 1) % BTRFS_INHIBITED_EBS_SLOTS; + } + slot = trans->inhibited_ebs_hand; + trans->inhibited_ebs_hand = (trans->inhibited_ebs_hand + 1) % BTRFS_INHIBITED_EBS_SLOTS; + + atomic_dec(&trans->inhibited_ebs[slot]->writeback_inhibitors); + free_extent_buffer(trans->inhibited_ebs[slot]); + + return slot; +} + /* * Inhibit writeback on buffer during transaction. * * @trans: transaction handle that will own the inhibitor * @eb: extent buffer to inhibit writeback on * - * Attempt to track this extent buffer in the transaction's inhibited set. If - * memory allocation fails, the buffer is simply not tracked. It may be written - * back and need re-COW, which is the original behavior. This is acceptable - * since inhibiting writeback is an optimization. + * Attempt to track this extent buffer in the transaction's inhibited set. When + * the set is full the coldest tracked buffer is evicted instead. An untracked + * buffer may be written back and need re-COW, which is the original behavior. + * This is acceptable since inhibiting writeback is an optimization. */ void btrfs_inhibit_eb_writeback(struct btrfs_trans_handle *trans, struct extent_buffer *eb) { - unsigned long index = eb->start >> trans->fs_info->nodesize_bits; - void *old; + int slot; lockdep_assert_held(&eb->lock); - /* Check if already inhibited by this handle. */ - old = xa_load(&trans->writeback_inhibited_ebs, index); - if (old == eb) - return; - /* Take reference for the xarray entry. */ + /* Already tracked: set its reference bit (second chance) and return. */ + for (int i = 0; i < trans->nr_inhibited_ebs; i++) { + if (trans->inhibited_ebs[i] == eb) { + trans->inhibited_ebs_referenced |= 1U << i; + return; + } + } + + slot = btrfs_inhibit_claim_slot(trans); + + /* + * Pin the eb while the array holds a raw pointer to it; the counter is + * what lock_extent_buffer_for_io() checks. + */ refcount_inc(&eb->refs); - - old = xa_store(&trans->writeback_inhibited_ebs, index, eb, GFP_NOFS); - if (xa_is_err(old)) { - /* Allocation failed, just skip inhibiting this buffer. */ - free_extent_buffer(eb); - return; - } - - /* Handle replacement of different eb at same index. */ - if (old && old != eb) { - struct extent_buffer *old_eb = old; - - atomic_dec(&old_eb->writeback_inhibitors); - free_extent_buffer(old_eb); - } - atomic_inc(&eb->writeback_inhibitors); + trans->inhibited_ebs[slot] = eb; + trans->inhibited_ebs_referenced |= 1U << slot; } /* @@ -3188,14 +3212,13 @@ void btrfs_inhibit_eb_writeback(struct btrfs_trans_handle *trans, struct extent_ */ void btrfs_uninhibit_all_eb_writeback(struct btrfs_trans_handle *trans) { - struct extent_buffer *eb; - unsigned long index; - - xa_for_each(&trans->writeback_inhibited_ebs, index, eb) { - atomic_dec(&eb->writeback_inhibitors); - free_extent_buffer(eb); + for (int i = 0; i < trans->nr_inhibited_ebs; i++) { + atomic_dec(&trans->inhibited_ebs[i]->writeback_inhibitors); + free_extent_buffer(trans->inhibited_ebs[i]); } - xa_destroy(&trans->writeback_inhibited_ebs); + trans->nr_inhibited_ebs = 0; + trans->inhibited_ebs_referenced = 0; + trans->inhibited_ebs_hand = 0; } static struct extent_buffer *__alloc_extent_buffer(struct btrfs_fs_info *fs_info, diff --git a/fs/btrfs/transaction.c b/fs/btrfs/transaction.c index 8f9419728100..45149d027740 100644 --- a/fs/btrfs/transaction.c +++ b/fs/btrfs/transaction.c @@ -698,8 +698,6 @@ start_transaction(struct btrfs_root *root, unsigned int num_items, goto alloc_fail; } - xa_init(&h->writeback_inhibited_ebs); - /* * If we are JOIN_NOLOCK we're already committing a transaction and * waiting on this guy, so we don't need to do the sb_start_intwrite diff --git a/fs/btrfs/transaction.h b/fs/btrfs/transaction.h index 5e4b1106fd90..3a57f227b5ed 100644 --- a/fs/btrfs/transaction.h +++ b/fs/btrfs/transaction.h @@ -7,12 +7,12 @@ #define BTRFS_TRANSACTION_H #include +#include #include #include #include #include #include -#include #include "btrfs_inode.h" #include "delayed-ref.h" @@ -23,6 +23,7 @@ struct btrfs_fs_info; struct btrfs_root_item; struct btrfs_root; struct btrfs_path; +struct extent_buffer; /* * Signal that a direct IO write is in progress, to avoid deadlock for sync @@ -136,6 +137,18 @@ enum { #define TRANS_EXTWRITERS (__TRANS_START | __TRANS_ATTACH) +/* + * Number of extent buffers a transaction handle tracks for writeback + * inhibition. The CLOCK reference bits pack into a u32 so this must not exceed + * 32, and keeping it a power of two lets the compiler reduce the CLOCK hand + * modulo to a mask. + */ +#define BTRFS_INHIBITED_EBS_SLOTS 8 + +static_assert(BTRFS_INHIBITED_EBS_SLOTS <= 32); +static_assert(BTRFS_INHIBITED_EBS_SLOTS != 0 && + (BTRFS_INHIBITED_EBS_SLOTS & (BTRFS_INHIBITED_EBS_SLOTS - 1)) == 0); + struct btrfs_trans_handle { u64 transid; u64 bytes_reserved; @@ -163,8 +176,14 @@ struct btrfs_trans_handle { struct btrfs_fs_info *fs_info; struct list_head new_bgs; struct btrfs_block_rsv delayed_rsv; - /* Extent buffers with writeback inhibited by this handle. */ - struct xarray writeback_inhibited_ebs; + + /* Extent buffers this handle has inhibited writeback on. */ + struct extent_buffer *inhibited_ebs[BTRFS_INHIBITED_EBS_SLOTS]; + /* CLOCK reference bit per slot. */ + u32 inhibited_ebs_referenced; + u32 nr_inhibited_ebs; + /* CLOCK hand. */ + u32 inhibited_ebs_hand; }; /* From 4d36517021cd9e5929abeaf879a1f097b70c1d16 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Mon, 20 Jul 2026 18:26:52 +0930 Subject: [PATCH 49/72] btrfs: open code BTRFS_BYTES_TO_BLKS() That macro is only utilized 4 times, all inside file.c, while we have tons of open-coded usages. And since it's a macro, there is no proper type checks at all. There isn't much need for such a rarely utilized macro. Signed-off-by: Qu Wenruo Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/file.c | 8 ++++---- fs/btrfs/fs.h | 2 -- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/fs/btrfs/file.c b/fs/btrfs/file.c index 8f078c58e940..818c53c445f8 100644 --- a/fs/btrfs/file.c +++ b/fs/btrfs/file.c @@ -2661,8 +2661,8 @@ static int btrfs_punch_hole(struct file *file, loff_t offset, loff_t len) lockstart = round_up(offset, fs_info->sectorsize); lockend = round_down(offset + len, fs_info->sectorsize) - 1; - same_block = (BTRFS_BYTES_TO_BLKS(fs_info, offset)) - == (BTRFS_BYTES_TO_BLKS(fs_info, offset + len - 1)); + same_block = (offset >> fs_info->sectorsize_bits) == + ((offset + len - 1) >> fs_info->sectorsize_bits); /* * Only do this if we are in the same block and we aren't doing the * entire block. @@ -2945,8 +2945,8 @@ static int btrfs_zero_range(struct inode *inode, } btrfs_free_extent_map(em); - if (BTRFS_BYTES_TO_BLKS(fs_info, offset) == - BTRFS_BYTES_TO_BLKS(fs_info, offset + len - 1)) { + if ((offset >> fs_info->sectorsize_bits) == + ((offset + len - 1) >> fs_info->sectorsize_bits)) { em = btrfs_get_extent(BTRFS_I(inode), NULL, alloc_start, sectorsize); if (IS_ERR(em)) { ret = PTR_ERR(em); diff --git a/fs/btrfs/fs.h b/fs/btrfs/fs.h index dcadcf7cc813..10e15a319b93 100644 --- a/fs/btrfs/fs.h +++ b/fs/btrfs/fs.h @@ -1060,8 +1060,6 @@ static inline u64 btrfs_calc_metadata_size(const struct btrfs_fs_info *fs_info, #define BTRFS_MAX_EXTENT_ITEM_SIZE(r) ((BTRFS_LEAF_DATA_SIZE(r->fs_info) >> 4) - \ sizeof(struct btrfs_item)) -#define BTRFS_BYTES_TO_BLKS(fs_info, bytes) ((bytes) >> (fs_info)->sectorsize_bits) - static inline bool btrfs_is_zoned(const struct btrfs_fs_info *fs_info) { return IS_ENABLED(CONFIG_BLK_DEV_ZONED) && fs_info->zone_size > 0; From d34a3a8ba61e3b1f33e2fc8be963045bcfbbce79 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Wed, 22 Jul 2026 17:04:07 +0930 Subject: [PATCH 50/72] btrfs: use %pe for error code output During an interrupted mount, I got the following messages: workqueue: Failed to create a rescuer kthread for wq "btrfs-qgroup-rescan": -EINTR BTRFS error (device dm-3): open_ctree failed: -12 Workqueue code is outputting a human readable error string, meanwhile we're still using a numeric error code. So follow the workqueue code to use "%pe" format, which will automatically convert an error pointer to the human readable string. However this is a minor pitfall, if the return value is not an error code, e.g. a positive number, "%pe" with "ERR_PTR(ret)" will output the pointer as a hash value, e.g.: ret=1 %pe out=0000000019414716 ret=-22 %pe out=-EINVAL So we should not use this "%pe" output for callsites that are known to return positive values. Reviewed-by: Johannes Thumshirn Reviewed-by: Jeff Layton Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/delayed-inode.c | 12 +++---- fs/btrfs/disk-io.c | 76 +++++++++++++++++++++------------------- fs/btrfs/extent-tree.c | 20 +++++------ fs/btrfs/extent_io.c | 8 ++--- fs/btrfs/inode.c | 31 ++++++++-------- fs/btrfs/ioctl.c | 4 +-- fs/btrfs/messages.c | 10 +++--- fs/btrfs/qgroup.c | 8 ++--- fs/btrfs/root-tree.c | 6 ++-- fs/btrfs/super.c | 2 +- fs/btrfs/transaction.c | 8 ++--- fs/btrfs/verity.c | 2 +- 12 files changed, 94 insertions(+), 93 deletions(-) diff --git a/fs/btrfs/delayed-inode.c b/fs/btrfs/delayed-inode.c index 09795439b9fb..db2ffab0941a 100644 --- a/fs/btrfs/delayed-inode.c +++ b/fs/btrfs/delayed-inode.c @@ -1523,10 +1523,10 @@ int btrfs_insert_delayed_dir_index(struct btrfs_trans_handle *trans, ret = __btrfs_add_delayed_item(delayed_node, delayed_item); if (unlikely(ret)) { btrfs_err(trans->fs_info, -"error adding delayed dir index item, name: %.*s, index: %llu, root: %llu, dir: %llu, dir->index_cnt: %llu, delayed_node->index_cnt: %llu, error: %d", +"error adding delayed dir index item, name: %.*s, index: %llu, root: %llu, dir: %llu, dir->index_cnt: %llu, delayed_node->index_cnt: %llu, error: %pe", name_len, name, index, btrfs_root_id(delayed_node->root), delayed_node->inode_id, dir->index_cnt, - delayed_node->index_cnt, ret); + delayed_node->index_cnt, ERR_PTR(ret)); btrfs_release_delayed_item(delayed_item); btrfs_release_dir_index_item_space(trans); mutex_unlock(&delayed_node->mutex); @@ -1645,8 +1645,8 @@ int btrfs_delete_delayed_dir_index(struct btrfs_trans_handle *trans, */ if (ret < 0) { btrfs_err(trans->fs_info, -"metadata reservation failed for delayed dir item deletion, index: %llu, root: %llu, inode: %llu, error: %d", - index, btrfs_root_id(node->root), node->inode_id, ret); +"metadata reservation failed for delayed dir item deletion, index: %llu, root: %llu, inode: %llu, error: %pe", + index, btrfs_root_id(node->root), node->inode_id, ERR_PTR(ret)); btrfs_release_delayed_item(item); goto end; } @@ -1655,8 +1655,8 @@ int btrfs_delete_delayed_dir_index(struct btrfs_trans_handle *trans, ret = __btrfs_add_delayed_item(node, item); if (unlikely(ret)) { btrfs_err(trans->fs_info, -"failed to add delayed dir index item, root: %llu, inode: %llu, index: %llu, error: %d", - btrfs_root_id(node->root), node->inode_id, index, ret); +"failed to add delayed dir index item, root: %llu, inode: %llu, index: %llu, error: %pe", + btrfs_root_id(node->root), node->inode_id, index, ERR_PTR(ret)); btrfs_delayed_item_release_metadata(dir->root, item); btrfs_release_delayed_item(item); } diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c index d30c1d02d994..0357c84d6c25 100644 --- a/fs/btrfs/disk-io.c +++ b/fs/btrfs/disk-io.c @@ -2052,7 +2052,7 @@ static int btrfs_replay_log(struct btrfs_fs_info *fs_info, if (IS_ERR(log_tree_root->node)) { ret = PTR_ERR(log_tree_root->node); log_tree_root->node = NULL; - btrfs_err(fs_info, "failed to read log tree with error: %d", ret); + btrfs_err(fs_info, "failed to read log tree with error: %pe", ERR_PTR(ret)); btrfs_put_root(log_tree_root); return ret; } @@ -2062,7 +2062,7 @@ static int btrfs_replay_log(struct btrfs_fs_info *fs_info, btrfs_put_root(log_tree_root); if (unlikely(ret)) { ASSERT(BTRFS_FS_ERROR(fs_info) != 0); - btrfs_err(fs_info, "failed to recover log trees with error: %d", ret); + btrfs_err(fs_info, "failed to recover log trees with error: %pe", ERR_PTR(ret)); return ret; } @@ -2303,8 +2303,8 @@ static int btrfs_read_roots(struct btrfs_fs_info *fs_info) return 0; out: - btrfs_warn(fs_info, "failed to read root (objectid=%llu): %d", - location.objectid, ret); + btrfs_warn(fs_info, "failed to read root (objectid=%llu): %pe", + location.objectid, ERR_PTR(ret)); return ret; } @@ -2978,8 +2978,8 @@ static int btrfs_uuid_rescan_kthread(void *data) ret = btrfs_uuid_tree_iterate(fs_info); if (ret < 0) { if (ret != -EINTR) - btrfs_warn(fs_info, "iterating uuid_tree failed %d", - ret); + btrfs_warn(fs_info, "iterating uuid_tree failed %pe", + ERR_PTR(ret)); up(&fs_info->uuid_tree_rescan_sem); return ret; } @@ -3082,7 +3082,7 @@ int btrfs_start_pre_rw_mount(struct btrfs_fs_info *fs_info) ret = btrfs_rebuild_free_space_tree(fs_info); if (ret) { btrfs_warn(fs_info, - "failed to rebuild free space tree: %d", ret); + "failed to rebuild free space tree: %pe", ERR_PTR(ret)); return ret; } } @@ -3093,7 +3093,7 @@ int btrfs_start_pre_rw_mount(struct btrfs_fs_info *fs_info) ret = btrfs_delete_free_space_tree(fs_info); if (ret) { btrfs_warn(fs_info, - "failed to disable free space tree: %d", ret); + "failed to disable free space tree: %pe", ERR_PTR(ret)); return ret; } } @@ -3104,7 +3104,8 @@ int btrfs_start_pre_rw_mount(struct btrfs_fs_info *fs_info) */ ret = btrfs_delete_orphan_free_space_entries(fs_info); if (ret < 0) { - btrfs_err(fs_info, "failed to delete orphan free space tree entries: %d", ret); + btrfs_err(fs_info, "failed to delete orphan free space tree entries: %pe", + ERR_PTR(ret)); return ret; } /* @@ -3138,7 +3139,7 @@ int btrfs_start_pre_rw_mount(struct btrfs_fs_info *fs_info) ret = btrfs_recover_relocation(fs_info); mutex_unlock(&fs_info->cleaner_mutex); if (ret < 0) { - btrfs_warn(fs_info, "failed to recover relocation: %d", ret); + btrfs_warn(fs_info, "failed to recover relocation: %pe", ERR_PTR(ret)); return ret; } @@ -3148,7 +3149,7 @@ int btrfs_start_pre_rw_mount(struct btrfs_fs_info *fs_info) ret = btrfs_create_free_space_tree(fs_info); if (ret) { btrfs_warn(fs_info, - "failed to create free space tree: %d", ret); + "failed to create free space tree: %pe", ERR_PTR(ret)); return ret; } } @@ -3176,7 +3177,7 @@ int btrfs_start_pre_rw_mount(struct btrfs_fs_info *fs_info) ret = btrfs_create_uuid_tree(fs_info); if (ret) { btrfs_warn(fs_info, - "failed to create the UUID tree %d", ret); + "failed to create the UUID tree %pe", ERR_PTR(ret)); return ret; } } @@ -3557,7 +3558,7 @@ int __cold open_ctree(struct super_block *sb, struct btrfs_fs_devices *fs_device ret = btrfs_read_sys_array(fs_info); mutex_unlock(&fs_info->chunk_mutex); if (ret) { - btrfs_err(fs_info, "failed to read the system array: %d", ret); + btrfs_err(fs_info, "failed to read the system array: %pe", ERR_PTR(ret)); goto fail_sb_buffer; } @@ -3576,7 +3577,7 @@ int __cold open_ctree(struct super_block *sb, struct btrfs_fs_devices *fs_device ret = btrfs_read_chunk_tree(fs_info); if (ret) { - btrfs_err(fs_info, "failed to read chunk tree: %d", ret); + btrfs_err(fs_info, "failed to read chunk tree: %pe", ERR_PTR(ret)); goto fail_tree_roots; } @@ -3606,7 +3607,7 @@ int __cold open_ctree(struct super_block *sb, struct btrfs_fs_devices *fs_device ret = btrfs_get_dev_zone_info_all_devices(fs_info); if (ret) { btrfs_err(fs_info, - "zoned: failed to read device zone info: %d", ret); + "zoned: failed to read device zone info: %pe", ERR_PTR(ret)); goto fail_block_groups; } @@ -3629,72 +3630,73 @@ int __cold open_ctree(struct super_block *sb, struct btrfs_fs_devices *fs_device ret = btrfs_verify_dev_extents(fs_info); if (ret) { btrfs_err(fs_info, - "failed to verify dev extents against chunks: %d", - ret); + "failed to verify dev extents against chunks: %pe", + ERR_PTR(ret)); goto fail_block_groups; } ret = btrfs_recover_balance(fs_info); if (ret) { - btrfs_err(fs_info, "failed to recover balance: %d", ret); + btrfs_err(fs_info, "failed to recover balance: %pe", ERR_PTR(ret)); goto fail_block_groups; } ret = btrfs_init_dev_stats(fs_info); if (ret) { - btrfs_err(fs_info, "failed to init dev_stats: %d", ret); + btrfs_err(fs_info, "failed to init dev_stats: %pe", ERR_PTR(ret)); goto fail_block_groups; } ret = btrfs_init_dev_replace(fs_info); if (ret) { - btrfs_err(fs_info, "failed to init dev_replace: %d", ret); + btrfs_err(fs_info, "failed to init dev_replace: %pe", ERR_PTR(ret)); goto fail_block_groups; } ret = btrfs_check_zoned_mode(fs_info); if (ret) { - btrfs_err(fs_info, "failed to initialize zoned mode: %d", - ret); + btrfs_err(fs_info, "failed to initialize zoned mode: %pe", + ERR_PTR(ret)); goto fail_block_groups; } ret = btrfs_sysfs_add_fsid(fs_devices); if (ret) { - btrfs_err(fs_info, "failed to init sysfs fsid interface: %d", - ret); + btrfs_err(fs_info, "failed to init sysfs fsid interface: %pe", + ERR_PTR(ret)); goto fail_block_groups; } ret = btrfs_sysfs_add_mounted(fs_info); if (ret) { - btrfs_err(fs_info, "failed to init sysfs interface: %d", ret); + btrfs_err(fs_info, "failed to init sysfs interface: %pe", ERR_PTR(ret)); goto fail_fsdev_sysfs; } ret = btrfs_init_space_info(fs_info); if (ret) { - btrfs_err(fs_info, "failed to initialize space info: %d", ret); + btrfs_err(fs_info, "failed to initialize space info: %pe", ERR_PTR(ret)); goto fail_sysfs; } ret = btrfs_read_block_groups(fs_info); if (ret) { - btrfs_err(fs_info, "failed to read block groups: %d", ret); + btrfs_err(fs_info, "failed to read block groups: %pe", ERR_PTR(ret)); goto fail_sysfs; } if (btrfs_fs_incompat(fs_info, REMAP_TREE)) { ret = btrfs_populate_fully_remapped_bgs_list(fs_info); if (ret) { - btrfs_err(fs_info, "failed to populate fully_remapped_bgs list: %d", ret); + btrfs_err(fs_info, "failed to populate fully_remapped_bgs list: %pe", + ERR_PTR(ret)); goto fail_sysfs; } } ret = btrfs_init_writeback_bio_size(fs_info); if (ret) { - btrfs_err(fs_info, "failed to get optimum writeback size: %d", - ret); + btrfs_err(fs_info, "failed to get optimum writeback size: %pe", + ERR_PTR(ret)); goto fail_sysfs; } @@ -3750,7 +3752,7 @@ int __cold open_ctree(struct super_block *sb, struct btrfs_fs_devices *fs_device fs_info->fs_root = btrfs_get_fs_root(fs_info, BTRFS_FS_TREE_OBJECTID, true); if (IS_ERR(fs_info->fs_root)) { ret = PTR_ERR(fs_info->fs_root); - btrfs_err(fs_info, "failed to read fs tree: %d", ret); + btrfs_err(fs_info, "failed to read fs tree: %pe", ERR_PTR(ret)); fs_info->fs_root = NULL; goto fail_qgroup; } @@ -3771,7 +3773,7 @@ int __cold open_ctree(struct super_block *sb, struct btrfs_fs_devices *fs_device btrfs_info(fs_info, "checking UUID tree"); ret = btrfs_check_uuid_tree(fs_info); if (ret) { - btrfs_err(fs_info, "failed to check the UUID tree: %d", ret); + btrfs_err(fs_info, "failed to check the UUID tree: %pe", ERR_PTR(ret)); close_ctree(fs_info); return ret; } @@ -3887,8 +3889,8 @@ static int write_dev_supers(struct btrfs_device *device, continue; } else if (ret < 0) { btrfs_err(device->fs_info, - "couldn't get super block location for mirror %d error %d", - i, ret); + "couldn't get super block location for mirror %d error %pe", + i, ERR_PTR(ret)); atomic_inc(&device->sb_write_errors); continue; } @@ -3906,8 +3908,8 @@ static int write_dev_supers(struct btrfs_device *device, GFP_NOFS); if (IS_ERR(folio)) { btrfs_err(device->fs_info, - "couldn't get super block page for bytenr %llu error %ld", - bytenr, PTR_ERR(folio)); + "couldn't get super block page for bytenr %llu error %pe", + bytenr, folio); atomic_inc(&device->sb_write_errors); continue; } @@ -4534,7 +4536,7 @@ void __cold close_ctree(struct btrfs_fs_info *fs_info) if (!btrfs_is_shutdown(fs_info)) { ret = btrfs_commit_super(fs_info); if (ret) - btrfs_err(fs_info, "commit super block returned %d", ret); + btrfs_err(fs_info, "commit super block returned %pe", ERR_PTR(ret)); } } diff --git a/fs/btrfs/extent-tree.c b/fs/btrfs/extent-tree.c index 235381b31298..365735c54e56 100644 --- a/fs/btrfs/extent-tree.c +++ b/fs/btrfs/extent-tree.c @@ -5880,8 +5880,8 @@ static int maybe_drop_reference(struct btrfs_trans_handle *trans, struct btrfs_r ret = btrfs_qgroup_trace_subtree(trans, next, generation, level - 1); if (ret) { btrfs_err_rl(root->fs_info, -"error %d accounting shared subtree, quota is out of sync, rescan required", - ret); +"error %pe accounting shared subtree, quota is out of sync, rescan required", + ERR_PTR(ret)); } } @@ -6096,8 +6096,8 @@ static noinline int walk_up_proc(struct btrfs_trans_handle *trans, ret = btrfs_qgroup_trace_leaf_items(trans, eb); if (ret) { btrfs_err_rl(fs_info, - "error %d accounting leaf items, quota is out of sync, rescan required", - ret); + "error %pe accounting leaf items, quota is out of sync, rescan required", + ERR_PTR(ret)); } } } @@ -6498,8 +6498,8 @@ int btrfs_drop_snapshot(struct btrfs_root *root, bool update_ref, bool for_reloc ret = btrfs_qgroup_cleanup_dropped_subvolume(fs_info, rootid); if (ret < 0) btrfs_warn_rl(fs_info, - "failed to cleanup qgroup 0/%llu: %d", - rootid, ret); + "failed to cleanup qgroup 0/%llu: %pe", + rootid, ERR_PTR(ret)); ret = 0; } /* @@ -6914,8 +6914,8 @@ int btrfs_trim_fs(struct btrfs_fs_info *fs_info, struct fstrim_range *range) if (bg_failed) btrfs_warn(fs_info, - "failed to trim %llu block group(s), first error %d", - bg_failed, bg_ret); + "failed to trim %llu block group(s), first error %pe", + bg_failed, ERR_PTR(bg_ret)); if (ret == -ERESTARTSYS || ret == -EINTR) return ret; @@ -6925,8 +6925,8 @@ int btrfs_trim_fs(struct btrfs_fs_info *fs_info, struct fstrim_range *range) if (dev_failed) btrfs_warn(fs_info, - "failed to trim %llu device(s), first error %d", - dev_failed, dev_ret); + "failed to trim %llu device(s), first error %pe", + dev_failed, ERR_PTR(dev_ret)); range->len = trimmed; if (ret == -ERESTARTSYS || ret == -EINTR) return ret; diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c index 116cbc8a34fa..632637c49732 100644 --- a/fs/btrfs/extent_io.c +++ b/fs/btrfs/extent_io.c @@ -1698,13 +1698,13 @@ static noinline_for_stack int writepage_delalloc(struct btrfs_inode *inode, last_finished_delalloc_end = found_start + found_len; if (unlikely(ret < 0)) btrfs_err_rl(fs_info, -"failed to run delalloc range, root=%lld ino=%llu folio=%llu submit_bitmap=%*pbl start=%llu len=%u: %d", +"failed to run delalloc range, root=%lld ino=%llu folio=%llu submit_bitmap=%*pbl start=%llu len=%u: %pe", btrfs_root_id(inode->root), btrfs_ino(inode), folio_pos(folio), blocks_per_folio, bio_ctrl->submit_bitmap, - found_start, found_len, ret); + found_start, found_len, ERR_PTR(ret)); } else { /* * We've hit an error during previous delalloc range, @@ -2081,10 +2081,10 @@ static int extent_writepage(struct folio *folio, struct btrfs_bio_ctrl *bio_ctrl return 0; if (unlikely(ret < 0)) btrfs_err_rl(fs_info, -"failed to submit blocks, root=%lld inode=%llu folio=%llu submit_bitmap=%*pbl: %d", +"failed to submit blocks, root=%lld inode=%llu folio=%llu submit_bitmap=%*pbl: %pe", btrfs_root_id(inode->root), btrfs_ino(inode), folio_pos(folio), blocks_per_folio, - bio_ctrl->submit_bitmap, ret); + bio_ctrl->submit_bitmap, ERR_PTR(ret)); bio_ctrl->wbc->nr_to_write--; diff --git a/fs/btrfs/inode.c b/fs/btrfs/inode.c index 2a66bcb59ecb..50c6640543b9 100644 --- a/fs/btrfs/inode.c +++ b/fs/btrfs/inode.c @@ -250,8 +250,8 @@ static void print_data_reloc_error(const struct btrfs_inode *inode, u64 file_off ret = extent_from_logical(fs_info, logical, &path, &found_key, &flags); if (ret < 0) { - btrfs_err_rl(fs_info, "failed to lookup extent item for logical %llu: %d", - logical, ret); + btrfs_err_rl(fs_info, "failed to lookup extent item for logical %llu: %pe", + logical, ERR_PTR(ret)); return; } eb = path.nodes[0]; @@ -1019,9 +1019,10 @@ static void submit_uncompressed_range(struct btrfs_inode *inode, btrfs_folio_end_lock(inode->root->fs_info, locked_folio, start, async_extent->ram_size); btrfs_err_rl(inode->root->fs_info, - "%s failed, root=%llu inode=%llu start=%llu len=%llu: %d", + "%s failed, root=%llu inode=%llu start=%llu len=%llu: %pe", __func__, btrfs_root_id(inode->root), - btrfs_ino(inode), start, async_extent->ram_size, ret); + btrfs_ino(inode), start, async_extent->ram_size, + ERR_PTR(ret)); } } @@ -1508,10 +1509,10 @@ static noinline int cow_file_range(struct btrfs_inode *inode, end - start - cur_alloc_size + 1, NULL); } btrfs_err(fs_info, -"%s failed, root=%llu inode=%llu start=%llu len=%llu cur_offset=%llu cur_alloc_size=%u: %d", +"%s failed, root=%llu inode=%llu start=%llu len=%llu cur_offset=%llu cur_alloc_size=%u: %pe", __func__, btrfs_root_id(inode->root), btrfs_ino(inode), orig_start, end + 1 - orig_start, - start, cur_alloc_size, ret); + start, cur_alloc_size, ERR_PTR(ret)); return ret; } @@ -1962,9 +1963,9 @@ static int nocow_one_range(struct btrfs_inode *inode, struct folio *locked_folio PAGE_UNLOCK | PAGE_START_WRITEBACK | PAGE_END_WRITEBACK); btrfs_err(inode->root->fs_info, - "%s failed, root=%lld inode=%llu start=%llu len=%llu: %d", + "%s failed, root=%lld inode=%llu start=%llu len=%llu: %pe", __func__, btrfs_root_id(inode->root), btrfs_ino(inode), - file_pos, len, ret); + file_pos, len, ERR_PTR(ret)); return ret; } @@ -2285,10 +2286,10 @@ static noinline int run_delalloc_nocow(struct btrfs_inode *inode, } btrfs_free_path(path); btrfs_err(fs_info, -"%s failed, root=%llu inode=%llu start=%llu len=%llu cur_offset=%llu oe_cleanup=%llu oe_cleanup_len=%llu untouched_start=%llu untouched_len=%llu: %d", +"%s failed, root=%llu inode=%llu start=%llu len=%llu cur_offset=%llu oe_cleanup=%llu oe_cleanup_len=%llu untouched_start=%llu untouched_len=%llu: %pe", __func__, btrfs_root_id(inode->root), btrfs_ino(inode), start, end + 1 - start, cur_offset, oe_cleanup_start, oe_cleanup_len, - untouched_start, untouched_len, ret); + untouched_start, untouched_len, ERR_PTR(ret)); return ret; } @@ -3907,7 +3908,7 @@ int btrfs_orphan_cleanup(struct btrfs_root *root) out: if (ret) - btrfs_err(fs_info, "could not do orphan cleanup %d", ret); + btrfs_err(fs_info, "could not do orphan cleanup %pe", ERR_PTR(ret)); return ret; } @@ -4210,8 +4211,8 @@ static int btrfs_read_locked_inode(struct btrfs_inode *inode, struct btrfs_path ret = btrfs_load_inode_props(inode, path); if (ret) btrfs_err(fs_info, - "error loading props for ino %llu (root %llu): %d", - btrfs_ino(inode), btrfs_root_id(root), ret); + "error loading props for ino %llu (root %llu): %pe", + btrfs_ino(inode), btrfs_root_id(root), ERR_PTR(ret)); } /* @@ -6825,8 +6826,8 @@ int btrfs_create_new_inode(struct btrfs_trans_handle *trans, } if (ret) { btrfs_err(fs_info, - "error inheriting props for ino %llu (root %llu): %d", - btrfs_ino(BTRFS_I(inode)), btrfs_root_id(root), ret); + "error inheriting props for ino %llu (root %llu): %pe", + btrfs_ino(BTRFS_I(inode)), btrfs_root_id(root), ERR_PTR(ret)); } /* diff --git a/fs/btrfs/ioctl.c b/fs/btrfs/ioctl.c index 5cb927c6e53d..ebfb258161c8 100644 --- a/fs/btrfs/ioctl.c +++ b/fs/btrfs/ioctl.c @@ -2839,8 +2839,8 @@ static long btrfs_ioctl_default_subvol(struct file *file, void __user *argp) else ret = -ENOENT; btrfs_err(fs_info, - "could not find default diritem for dir %llu: %d", - dir_id, ret); + "could not find default diritem for dir %llu: %pe", + dir_id, ERR_PTR(ret)); goto out_free; } diff --git a/fs/btrfs/messages.c b/fs/btrfs/messages.c index 7c60c14e60fa..198d1747c80a 100644 --- a/fs/btrfs/messages.c +++ b/fs/btrfs/messages.c @@ -279,7 +279,6 @@ void __btrfs_panic(const struct btrfs_fs_info *fs_info, const char *function, unsigned int line, int error, const char *fmt, ...) { char *s_id = ""; - const char *errstr; struct va_format vaf = { .fmt = fmt }; va_list args; @@ -289,13 +288,12 @@ void __btrfs_panic(const struct btrfs_fs_info *fs_info, const char *function, va_start(args, fmt); vaf.va = &args; - errstr = btrfs_decode_error(error); if (fs_info && (btrfs_test_opt(fs_info, PANIC_ON_FATAL_ERROR))) - panic(KERN_CRIT "BTRFS panic (device %s) in %s:%d: %pV (errno=%d %s)\n", - s_id, function, line, &vaf, error, errstr); + panic(KERN_CRIT "BTRFS panic (device %s) in %s:%d: %pV (errno=%d %pe)\n", + s_id, function, line, &vaf, error, ERR_PTR(error)); - btrfs_crit(fs_info, "panic in %s:%d: %pV (errno=%d %s)", - function, line, &vaf, error, errstr); + btrfs_crit(fs_info, "panic in %s:%d: %pV (errno=%d %pe)", + function, line, &vaf, error, ERR_PTR(error)); va_end(args); /* Caller calls BUG() */ } diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c index 502fb4a55cb2..210af4d7d4b5 100644 --- a/fs/btrfs/qgroup.c +++ b/fs/btrfs/qgroup.c @@ -3915,8 +3915,8 @@ static void btrfs_qgroup_rescan_worker(struct btrfs_work *work) ret = PTR_ERR(trans); trans = NULL; btrfs_err(fs_info, - "fail to start transaction for status update: %d", - ret); + "fail to start transaction for status update: %pe", + ERR_PTR(ret)); } } else { trans = NULL; @@ -3931,7 +3931,7 @@ static void btrfs_qgroup_rescan_worker(struct btrfs_work *work) if (ret2 < 0) { ret = ret2; - btrfs_err(fs_info, "fail to update qgroup status: %d", ret); + btrfs_err(fs_info, "fail to update qgroup status: %pe", ERR_PTR(ret)); } } fs_info->qgroup_rescan_running = false; @@ -3952,7 +3952,7 @@ static void btrfs_qgroup_rescan_worker(struct btrfs_work *work) btrfs_info(fs_info, "qgroup scan completed%s", ret > 0 ? " (inconsistency flag cleared)" : ""); } else { - btrfs_err(fs_info, "qgroup scan failed with %d", ret); + btrfs_err(fs_info, "qgroup scan failed with %pe", ERR_PTR(ret)); } } diff --git a/fs/btrfs/root-tree.c b/fs/btrfs/root-tree.c index 90659b287d90..2e4c3efbd02f 100644 --- a/fs/btrfs/root-tree.c +++ b/fs/btrfs/root-tree.c @@ -265,15 +265,15 @@ int btrfs_find_orphan_roots(struct btrfs_fs_info *fs_info) if (IS_ERR(trans)) { ret = PTR_ERR(trans); btrfs_err(fs_info, - "failed to join transaction to delete orphan item: %d", - ret); + "failed to join transaction to delete orphan item: %pe", + ERR_PTR(ret)); return ret; } ret = btrfs_del_orphan_item(trans, tree_root, root_objectid); btrfs_end_transaction(trans); if (ret) { btrfs_err(fs_info, - "failed to delete root orphan item: %d", ret); + "failed to delete root orphan item: %pe", ERR_PTR(ret)); return ret; } continue; diff --git a/fs/btrfs/super.c b/fs/btrfs/super.c index b745794595d1..464129b1b0d4 100644 --- a/fs/btrfs/super.c +++ b/fs/btrfs/super.c @@ -973,7 +973,7 @@ static int btrfs_fill_super(struct super_block *sb, ret = open_ctree(sb, fs_devices); if (ret) { - btrfs_err(fs_info, "open_ctree failed: %d", ret); + btrfs_err(fs_info, "open_ctree failed: %pe", ERR_PTR(ret)); return ret; } diff --git a/fs/btrfs/transaction.c b/fs/btrfs/transaction.c index 45149d027740..a1a3043a2cad 100644 --- a/fs/btrfs/transaction.c +++ b/fs/btrfs/transaction.c @@ -1640,7 +1640,7 @@ static int qgroup_account_snapshot(struct btrfs_trans_handle *trans, ret = btrfs_write_and_wait_transaction(trans); if (unlikely(ret)) { btrfs_err(fs_info, -"error while writing out transaction during qgroup snapshot accounting: %d", ret); +"error while writing out transaction during qgroup snapshot accounting: %pe", ERR_PTR(ret)); return ret; } @@ -2586,7 +2586,7 @@ int btrfs_commit_transaction(struct btrfs_trans_handle *trans) ret = btrfs_write_and_wait_transaction(trans); if (unlikely(ret)) { - btrfs_err(fs_info, "error while writing out transaction: %d", ret); + btrfs_err(fs_info, "error while writing out transaction: %pe", ERR_PTR(ret)); mutex_unlock(&fs_info->tree_log_mutex); goto scrub_continue; } @@ -2747,8 +2747,8 @@ void __cold __btrfs_abort_transaction(struct btrfs_trans_handle *trans, WRITE_ONCE(trans->transaction->aborted, error); trace_btrfs_transaction_abort(trans); if (first_hit) { - btrfs_err(fs_info, "Transaction %llu aborted (error %d)", - trans->transid, error); + btrfs_err(fs_info, "Transaction %llu aborted (%pe)", + trans->transid, ERR_PTR(error)); if (error == -ENOSPC) btrfs_dump_space_info_for_trans_abort(fs_info); } diff --git a/fs/btrfs/verity.c b/fs/btrfs/verity.c index 983365a73541..ebada817bd33 100644 --- a/fs/btrfs/verity.c +++ b/fs/btrfs/verity.c @@ -638,7 +638,7 @@ static int btrfs_end_enable_verity(struct file *filp, const void *desc, rollback_ret = rollback_verity(inode); if (rollback_ret) btrfs_err(inode->root->fs_info, - "failed to rollback verity items: %d", rollback_ret); + "failed to rollback verity items: %pe", ERR_PTR(rollback_ret)); return ret; } From 8cc569696dac51fc62bb39b3b8f530582b916d29 Mon Sep 17 00:00:00 2001 From: Yichong Chen Date: Wed, 22 Jul 2026 10:54:35 +0800 Subject: [PATCH 51/72] btrfs: retry verity reads for not-uptodate Merkle folios btrfs_read_merkle_tree_page() can find a folio in the mapping that is not uptodate. After taking the folio lock, the current code treats that state as a read error and returns -EIO. That can make a previous transient read failure sticky. If the failed read left a not-uptodate folio in the mapping, later callers find that folio and fail instead of retrying the read. Keep the existing page-cache insertion and locking order, but retry the Merkle item read when a not-uptodate folio is found in the mapping. Also unlock the folio when read_key_bytes() fails so that a later caller can lock it and retry the read. Fixes: 06ed09351b67 ("btrfs: convert btrfs_read_merkle_tree_page() to use a folio") Reviewed-by: Boris Burkov Signed-off-by: Yichong Chen Signed-off-by: David Sterba --- fs/btrfs/verity.c | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/fs/btrfs/verity.c b/fs/btrfs/verity.c index ebada817bd33..4e0ab5842274 100644 --- a/fs/btrfs/verity.c +++ b/fs/btrfs/verity.c @@ -720,14 +720,18 @@ static struct page *btrfs_read_merkle_tree_page(struct inode *inode, goto out; folio_lock(folio); - /* If it's not uptodate after we have the lock, we got a read error. */ - if (!folio_test_uptodate(folio)) { + /* Folio was truncated from mapping. */ + if (!folio->mapping) { folio_unlock(folio); folio_put(folio); - return ERR_PTR(-EIO); + goto again; } - folio_unlock(folio); - goto out; + /* Another reader may have filled the folio while we waited. */ + if (folio_test_uptodate(folio)) { + folio_unlock(folio); + goto out; + } + goto read_folio; } folio = filemap_alloc_folio(mapping_gfp_constraint(inode->i_mapping, ~__GFP_FS), @@ -744,6 +748,7 @@ static struct page *btrfs_read_merkle_tree_page(struct inode *inode, return ERR_PTR(ret); } +read_folio: /* * Merkle item keys are indexed from byte 0 in the merkle tree. * They have the form: @@ -753,6 +758,7 @@ static struct page *btrfs_read_merkle_tree_page(struct inode *inode, ret = read_key_bytes(BTRFS_I(inode), BTRFS_VERITY_MERKLE_ITEM_KEY, off, folio_address(folio), PAGE_SIZE, folio); if (ret < 0) { + folio_unlock(folio); folio_put(folio); return ERR_PTR(ret); } From a26f792036789a0ed6e4530967eb6013b69f0ca5 Mon Sep 17 00:00:00 2001 From: Zenghui Yu Date: Sun, 21 Jun 2026 15:46:26 +0800 Subject: [PATCH 52/72] btrfs: sysfs: fix path of the "read_policy" module parameter in comment The correct path of the "read_policy" module parameter should be /sys/module/btrfs/parameters/read_policy. Fix it. Acked-by: Randy Dunlap Signed-off-by: Zenghui Yu Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/sysfs.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/btrfs/sysfs.c b/fs/btrfs/sysfs.c index 0d14570c8bc2..39cb01ee441a 100644 --- a/fs/btrfs/sysfs.c +++ b/fs/btrfs/sysfs.c @@ -1336,7 +1336,7 @@ char *btrfs_get_mod_read_policy(void) return read_policy; } -/* Set perms to 0, disable /sys/module/btrfs/parameter/read_policy interface. */ +/* Set perms to 0, disable /sys/module/btrfs/parameters/read_policy interface. */ module_param(read_policy, charp, 0); MODULE_PARM_DESC(read_policy, "Global read policy: pid (default), round-robin[:], devid[:]"); From 5093038fc21d9b0f8211c28b90c7566397ac88a3 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Fri, 3 Jul 2026 17:13:38 +0100 Subject: [PATCH 53/72] btrfs: stop sleeping for one jiffy in non-ssd mounts during log commit Joining/starting a log transaction tracks if we ever had more than one task concurrently logging by setting the flag BTRFS_ROOT_MULTI_LOG_TASKS in the respective root. Once set, this flag remains for the rest of the lifetime of the transaction, only cleared when we don't have a log root and need to create a new one (transaction commits drop log roots). During log commit, if we are not on a ssd mount (or use the -o nossd mount option) and the BTRFS_ROOT_MULTI_LOG_TASKS flag is set, we sleep for one jiffy with the excuse to allow future log writers to join and log inodes and then commit a larger log transaction to reduce overall IO. However this is extremely inefficient because: 1) If at some point we had multiple tasks logging concurrently but now we have only one task at a time, we force it to wait for 1 jiffy; 2) One jiffy can vary between 1ms to 10ms, depending on the kernel config option CONFIG_HZ, which by default has a value of 250HZ and that corresponds to 4ms - that is a lot. This massively reduces the latency of fsyncs for non-ssd mounts, even on consumer grade spinning disks. Remove this mechanism to track if we have (or ever had) multiple tasks logging and wait for 1 jiffy. The following fio test was used to benchmark: $ cat fio-buffered-fsync.sh DEV=/dev/sdj MNT=/mnt/sdj MOUNT_OPTIONS="" MKFS_OPTIONS="" if [ $# -ne 6 ]; then echo "Use $0 NUM_JOBS FILE_SIZE IO_SIZE FSYNC_FREQ BLOCK_SIZE [write|randwrite]" exit 1 fi NUM_JOBS=$1 FILE_SIZE=$2 IO_SIZE=$3 FSYNC_FREQ=$4 BLOCK_SIZE=$5 WRITE_MODE=$6 if [ "$WRITE_MODE" != "write" ] && [ "$WRITE_MODE" != "randwrite" ]; then echo "Invalid WRITE_MODE, must be 'write' or 'randwrite'" exit 1 fi cat < /tmp/fio-job.ini [writers] rw=$WRITE_MODE fsync=$FSYNC_FREQ fallocate=none group_reporting=1 direct=0 bs=$BLOCK_SIZE ioengine=psync filesize=$FILE_SIZE io_size=$IO_SIZE directory=$MNT numjobs=$NUM_JOBS EOF echo echo "Using config:" echo cat /tmp/fio-job.ini echo umount $MNT &> /dev/null mkfs.btrfs -f $MKFS_OPTIONS $DEV mount $MOUNT_OPTIONS $DEV $MNT fio /tmp/fio-job.ini umount $MNT Running the script as: ./fio-buffered-fsync.sh 8 64M 64M 1 4K randwrite Before patch: WRITE: bw=2647KiB/s (2711kB/s), 2647KiB/s-2647KiB/s (2711kB/s-2711kB/s), io=512MiB (537MB), run=198055-198055msec After patch: WRITE: bw=14.9MiB/s (15.6MB/s), 14.9MiB/s-14.9MiB/s (15.6MB/s-15.6MB/s), io=512MiB (537MB), run=34471-34471msec That's about 5.7 times faster. Reviewed-by: Boris Burkov Reviewed-by: Jeff Layton Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/ctree.h | 2 -- fs/btrfs/tree-log.c | 18 +----------------- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/fs/btrfs/ctree.h b/fs/btrfs/ctree.h index 6de7ad191e04..0f2653182405 100644 --- a/fs/btrfs/ctree.h +++ b/fs/btrfs/ctree.h @@ -131,7 +131,6 @@ enum { BTRFS_ROOT_ORPHAN_ITEM_INSERTED, BTRFS_ROOT_DEFRAG_RUNNING, BTRFS_ROOT_FORCE_COW, - BTRFS_ROOT_MULTI_LOG_TASKS, BTRFS_ROOT_DIRTY, BTRFS_ROOT_DELETING, @@ -216,7 +215,6 @@ struct btrfs_root { * to access this field. */ int last_log_commit; - pid_t log_start_pid; u64 last_trans; diff --git a/fs/btrfs/tree-log.c b/fs/btrfs/tree-log.c index 875e4ddc68ea..ccc262833a7c 100644 --- a/fs/btrfs/tree-log.c +++ b/fs/btrfs/tree-log.c @@ -316,13 +316,6 @@ static int start_log_trans(struct btrfs_trans_handle *trans, wait_log_commit(root, root->log_transid - 1); goto again; } - - if (!root->log_start_pid) { - clear_bit(BTRFS_ROOT_MULTI_LOG_TASKS, &root->state); - root->log_start_pid = current->pid; - } else if (root->log_start_pid != current->pid) { - set_bit(BTRFS_ROOT_MULTI_LOG_TASKS, &root->state); - } } else { /* * This means fs_info->log_root_tree was already created @@ -340,8 +333,6 @@ static int start_log_trans(struct btrfs_trans_handle *trans, goto out; set_bit(BTRFS_ROOT_HAS_LOG_TREE, &root->state); - clear_bit(BTRFS_ROOT_MULTI_LOG_TASKS, &root->state); - root->log_start_pid = current->pid; } atomic_inc(&root->log_writers); @@ -3347,13 +3338,7 @@ int btrfs_sync_log(struct btrfs_trans_handle *trans, while (1) { int batch = atomic_read(&root->log_batch); - /* when we're on an ssd, just kick the log commit out */ - if (!btrfs_test_opt(fs_info, SSD) && - test_bit(BTRFS_ROOT_MULTI_LOG_TASKS, &root->state)) { - mutex_unlock(&root->log_mutex); - schedule_timeout_uninterruptible(1); - mutex_lock(&root->log_mutex); - } + wait_for_writer(root); if (batch == atomic_read(&root->log_batch)) break; @@ -3414,7 +3399,6 @@ int btrfs_sync_log(struct btrfs_trans_handle *trans, btrfs_set_root_log_transid(root, root->log_transid + 1); log->log_transid = root->log_transid; - root->log_start_pid = 0; /* * IO has been started, blocks of the log tree have WRITTEN flag set * in their headers. new modifications of the log will be written to From 4bdec1b14b1f0942de763215b4102bbdc090c752 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Tue, 14 Jul 2026 15:40:09 +0100 Subject: [PATCH 54/72] btrfs: remove log batch counter use for fsync We have the log batch counter defined per root which is now useless after the previous patch (titled: "btrfs: stop sleeping for one jiffy in non-ssd mounts during log commit"). The counter is incremented early in the fsync path, before and after flushing dellaloc and waiting for writeback, and then the counter is read during the log sync path. The goal was to wait for tasks that are about to join a log transaction, so that we could reduce the amount of IO and log syncing (flush all log tree extent buffers and write super blocks), but that mechanism does not work since if there are currently no log writers, btrfs_sync_log() does not unlock the root's log_mutex, so no new log writers can join the log transaction. Having concurrent fsync tasks increasing the log_batch counter only makes us loop unnecessarily in btrfs_sync_log() - that is always true since the previous patch mentioned above and was true before that patch only when not using the "-o ssd" mount option (which is activated by default if the filesystem does not have rotational devices). So remove the log batch counter. No performance changes were observed after removing it. Reviewed-by: Boris Burkov Reviewed-by: Jeff Layton Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/ctree.h | 2 -- fs/btrfs/disk-io.c | 1 - fs/btrfs/file.c | 4 ---- fs/btrfs/tree-log.c | 8 +------- 4 files changed, 1 insertion(+), 14 deletions(-) diff --git a/fs/btrfs/ctree.h b/fs/btrfs/ctree.h index 0f2653182405..d5d8b3899258 100644 --- a/fs/btrfs/ctree.h +++ b/fs/btrfs/ctree.h @@ -196,8 +196,6 @@ struct btrfs_root { /* Used only for log trees of subvolumes, not for the log root tree */ atomic_t log_writers; atomic_t log_commit[2]; - /* Used only for log trees of subvolumes, not for the log root tree */ - atomic_t log_batch; /* * Protected by the 'log_mutex' lock but can be read without holding * that lock to avoid unnecessary lock contention, in which case it diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c index 0357c84d6c25..a62748fc2881 100644 --- a/fs/btrfs/disk-io.c +++ b/fs/btrfs/disk-io.c @@ -669,7 +669,6 @@ static struct btrfs_root *btrfs_alloc_root(struct btrfs_fs_info *fs_info, atomic_set(&root->log_commit[0], 0); atomic_set(&root->log_commit[1], 0); atomic_set(&root->log_writers, 0); - atomic_set(&root->log_batch, 0); refcount_set(&root->refs, 1); atomic_set(&root->snapshot_force_cow, 0); atomic_set(&root->nr_swapfiles, 0); diff --git a/fs/btrfs/file.c b/fs/btrfs/file.c index 818c53c445f8..20e15dc30bfb 100644 --- a/fs/btrfs/file.c +++ b/fs/btrfs/file.c @@ -1573,8 +1573,6 @@ int btrfs_sync_file(struct file *file, loff_t start, loff_t end, int datasync) else btrfs_inode_lock(inode, BTRFS_ILOCK_MMAP); - atomic_inc(&root->log_batch); - /* * Before we acquired the inode's lock and the mmap lock, someone may * have dirtied more pages in the target range. We need to make sure @@ -1657,8 +1655,6 @@ int btrfs_sync_file(struct file *file, loff_t start, loff_t end, int datasync) if (ret) goto out_release_extents; - atomic_inc(&root->log_batch); - if (skip_inode_logging(&ctx)) { /* * We've had everything committed since the last time we were diff --git a/fs/btrfs/tree-log.c b/fs/btrfs/tree-log.c index ccc262833a7c..580f2aabc065 100644 --- a/fs/btrfs/tree-log.c +++ b/fs/btrfs/tree-log.c @@ -3336,13 +3336,7 @@ int btrfs_sync_log(struct btrfs_trans_handle *trans, if (atomic_read(&root->log_commit[(index1 + 1) % 2])) wait_log_commit(root, log_transid - 1); - while (1) { - int batch = atomic_read(&root->log_batch); - - wait_for_writer(root); - if (batch == atomic_read(&root->log_batch)) - break; - } + wait_for_writer(root); /* bail out if we need to do a full commit */ if (btrfs_need_log_full_commit(trans)) { From 31ffb32245be3049f040bf65aaba33fef3a39b21 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Wed, 15 Jul 2026 16:45:41 +0100 Subject: [PATCH 55/72] btrfs: move condition for log commit wait into wait_log_commit() Instead of having every caller check for root->log_commit[] being non-zero and then call wait_log_commit(), move the check into wait_log_commit() and have the callers call it unconditionally. Reviewed-by: Boris Burkov Reviewed-by: Jeff Layton Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/tree-log.c | 43 +++++++++++++++++-------------------------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/fs/btrfs/tree-log.c b/fs/btrfs/tree-log.c index 580f2aabc065..495da98a211d 100644 --- a/fs/btrfs/tree-log.c +++ b/fs/btrfs/tree-log.c @@ -221,7 +221,7 @@ static int btrfs_log_inode(struct btrfs_trans_handle *trans, static int link_to_fixup_dir(struct walk_control *wc, u64 objectid); static noinline int replay_dir_deletes(struct walk_control *wc, u64 dirid, bool del_all); -static void wait_log_commit(struct btrfs_root *root, int transid); +static bool wait_log_commit(struct btrfs_root *root, int transid); /* * tree logging is a special write ahead log used to make sure that @@ -305,17 +305,13 @@ static int start_log_trans(struct btrfs_trans_handle *trans, again: if (root->log_root) { - int index = (root->log_transid + 1) % 2; - if (btrfs_need_log_full_commit(trans)) { ret = BTRFS_LOG_FORCE_COMMIT; goto out; } - if (zoned && atomic_read(&root->log_commit[index])) { - wait_log_commit(root, root->log_transid - 1); + if (zoned && wait_log_commit(root, root->log_transid - 1)) goto again; - } } else { /* * This means fs_info->log_root_tree was already created @@ -363,13 +359,9 @@ static int join_running_log_trans(struct btrfs_root *root) mutex_lock(&root->log_mutex); again: if (root->log_root) { - int index = (root->log_transid + 1) % 2; - ret = 0; - if (zoned && atomic_read(&root->log_commit[index])) { - wait_log_commit(root, root->log_transid - 1); + if (zoned && wait_log_commit(root, root->log_transid - 1)) goto again; - } atomic_inc(&root->log_writers); } mutex_unlock(&root->log_mutex); @@ -3172,10 +3164,14 @@ static int update_log_root(struct btrfs_trans_handle *trans, return ret; } -static void wait_log_commit(struct btrfs_root *root, int transid) +/* Returns true if we had to wait, false otherwise. */ +static bool wait_log_commit(struct btrfs_root *root, int transid) { DEFINE_WAIT(wait); - int index = transid % 2; + const int index = (transid >= 0 ? transid % 2 : -transid % 2); + + if (atomic_read(&root->log_commit[index]) == 0) + return false; /* * we only allow two pending log transactions at a time, @@ -3195,6 +3191,8 @@ static void wait_log_commit(struct btrfs_root *root, int transid) mutex_lock(&root->log_mutex); } finish_wait(&root->log_commit_wait[index], &wait); + + return true; } static void wait_for_writer(struct btrfs_root *root) @@ -3298,15 +3296,15 @@ static inline void btrfs_remove_all_log_ctxs(struct btrfs_root *root, int btrfs_sync_log(struct btrfs_trans_handle *trans, struct btrfs_root *root, struct btrfs_log_ctx *ctx) { - int index1; - int index2; int mark; int ret; struct btrfs_fs_info *fs_info = root->fs_info; struct btrfs_root *log = root->log_root; struct btrfs_root *log_root_tree = fs_info->log_root_tree; struct btrfs_root_item new_root_item; - int log_transid = 0; + int log_transid = ctx->log_transid; + int index1 = log_transid % 2; + int index2; struct btrfs_log_ctx root_log_ctx; struct blk_plug plug; u64 log_root_start; @@ -3314,16 +3312,13 @@ int btrfs_sync_log(struct btrfs_trans_handle *trans, mutex_lock(&root->log_mutex); trace_btrfs_sync_log_enter(trans, root, ctx); - log_transid = ctx->log_transid; if (root->log_transid_committed >= log_transid) { trace_btrfs_sync_log_exit(trans, root, ctx, ctx->log_ret); mutex_unlock(&root->log_mutex); return ctx->log_ret; } - index1 = log_transid % 2; - if (atomic_read(&root->log_commit[index1])) { - wait_log_commit(root, log_transid); + if (wait_log_commit(root, log_transid)) { trace_btrfs_sync_log_exit(trans, root, ctx, ctx->log_ret); mutex_unlock(&root->log_mutex); return ctx->log_ret; @@ -3333,8 +3328,7 @@ int btrfs_sync_log(struct btrfs_trans_handle *trans, atomic_set(&root->log_commit[index1], 1); /* wait for previous tree log sync to complete */ - if (atomic_read(&root->log_commit[(index1 + 1) % 2])) - wait_log_commit(root, log_transid - 1); + wait_log_commit(root, log_transid - 1); wait_for_writer(root); @@ -3467,10 +3461,7 @@ int btrfs_sync_log(struct btrfs_trans_handle *trans, root_log_ctx.log_transid, log_root_tree->log_transid); atomic_set(&log_root_tree->log_commit[index2], 1); - if (atomic_read(&log_root_tree->log_commit[(index2 + 1) % 2])) { - wait_log_commit(log_root_tree, - root_log_ctx.log_transid - 1); - } + wait_log_commit(log_root_tree, root_log_ctx.log_transid - 1); /* * now that we've moved on to the tree of log tree roots, From 3ccdd23e9f8f3e94981e6472645442dfd6a789ea Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Thu, 16 Jul 2026 15:27:34 +0100 Subject: [PATCH 56/72] btrfs: check for exit condition after waking in wait_log_commit() We check for the exit condition after we add ourselves to the wait queue and before we unlock the root's log_mutex, sleep and lock again log_mutex. This is not incorrect, but it's not optimal since in the first iteration this is pointless because we already know that root->log_commit[index] is not zero, so we should check the exit condition only after unlocking log_mutex, sleeping, waking up and locking again the log_mutex. So move the check for the exit condition to bottom of the loop, after we were woken and locked log_mutex again. Reviewed-by: Boris Burkov Reviewed-by: Jeff Layton Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/tree-log.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fs/btrfs/tree-log.c b/fs/btrfs/tree-log.c index 495da98a211d..7d7a0dbc7b4a 100644 --- a/fs/btrfs/tree-log.c +++ b/fs/btrfs/tree-log.c @@ -3182,13 +3182,13 @@ static bool wait_log_commit(struct btrfs_root *root, int transid) prepare_to_wait(&root->log_commit_wait[index], &wait, TASK_UNINTERRUPTIBLE); - if (!(root->log_transid_committed < transid && - atomic_read(&root->log_commit[index]))) - break; - mutex_unlock(&root->log_mutex); schedule(); mutex_lock(&root->log_mutex); + + if (!(root->log_transid_committed < transid && + atomic_read(&root->log_commit[index]) != 0)) + break; } finish_wait(&root->log_commit_wait[index], &wait); From 12d2f44bdfce4cee463415a0392cb7aaeb53f255 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Fri, 17 Jul 2026 17:52:43 +0100 Subject: [PATCH 57/72] btrfs: use simple booleans for log_commit field in struct btrfs_root We are using atomic types for the log_commit array of struct btrfs_root but all we need is simple booleans. The log_commit array elements are always protected by the root's log_mutex, both for writes and reads, so we can use a simple boolean. The use of atomics if from the very early days of the log tree code where the access to the fields was not protected by any lock. So switch to simple booleans, which results in cheaper code and slightly reduces the object size too. Reviewed-by: Boris Burkov Reviewed-by: Jeff Layton Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/ctree.h | 2 +- fs/btrfs/disk-io.c | 2 -- fs/btrfs/transaction.c | 8 ++------ fs/btrfs/tree-log.c | 16 ++++++++-------- include/trace/events/btrfs.h | 4 ++-- 5 files changed, 13 insertions(+), 19 deletions(-) diff --git a/fs/btrfs/ctree.h b/fs/btrfs/ctree.h index d5d8b3899258..22ba2b4505b3 100644 --- a/fs/btrfs/ctree.h +++ b/fs/btrfs/ctree.h @@ -195,7 +195,7 @@ struct btrfs_root { struct list_head log_ctxs[2]; /* Used only for log trees of subvolumes, not for the log root tree */ atomic_t log_writers; - atomic_t log_commit[2]; + bool log_commit[2]; /* * Protected by the 'log_mutex' lock but can be read without holding * that lock to avoid unnecessary lock contention, in which case it diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c index a62748fc2881..e7433294906e 100644 --- a/fs/btrfs/disk-io.c +++ b/fs/btrfs/disk-io.c @@ -666,8 +666,6 @@ static struct btrfs_root *btrfs_alloc_root(struct btrfs_fs_info *fs_info, init_waitqueue_head(&root->log_commit_wait[1]); INIT_LIST_HEAD(&root->log_ctxs[0]); INIT_LIST_HEAD(&root->log_ctxs[1]); - atomic_set(&root->log_commit[0], 0); - atomic_set(&root->log_commit[1], 0); atomic_set(&root->log_writers, 0); refcount_set(&root->refs, 1); atomic_set(&root->snapshot_force_cow, 0); diff --git a/fs/btrfs/transaction.c b/fs/btrfs/transaction.c index a1a3043a2cad..bafc62cf5ebc 100644 --- a/fs/btrfs/transaction.c +++ b/fs/btrfs/transaction.c @@ -1517,12 +1517,8 @@ static noinline int commit_fs_roots(struct btrfs_trans_handle *trans) ASSERT(atomic_read(&root->log_writers) == 0, "atomic_read(&root->log_writers)=%d", atomic_read(&root->log_writers)); - ASSERT(atomic_read(&root->log_commit[0]) == 0, - "atomic_read(&root->log_commit[0])=%d", - atomic_read(&root->log_commit[0])); - ASSERT(atomic_read(&root->log_commit[1]) == 0, - "atomic_read(&root->log_commit[1])=%d", - atomic_read(&root->log_commit[1])); + ASSERT(!root->log_commit[0]); + ASSERT(!root->log_commit[1]); radix_tree_tag_clear(&fs_info->fs_roots_radix, (unsigned long)btrfs_root_id(root), diff --git a/fs/btrfs/tree-log.c b/fs/btrfs/tree-log.c index 7d7a0dbc7b4a..47046dd14997 100644 --- a/fs/btrfs/tree-log.c +++ b/fs/btrfs/tree-log.c @@ -3170,7 +3170,7 @@ static bool wait_log_commit(struct btrfs_root *root, int transid) DEFINE_WAIT(wait); const int index = (transid >= 0 ? transid % 2 : -transid % 2); - if (atomic_read(&root->log_commit[index]) == 0) + if (!root->log_commit[index]) return false; /* @@ -3187,7 +3187,7 @@ static bool wait_log_commit(struct btrfs_root *root, int transid) mutex_lock(&root->log_mutex); if (!(root->log_transid_committed < transid && - atomic_read(&root->log_commit[index]) != 0)) + root->log_commit[index])) break; } finish_wait(&root->log_commit_wait[index], &wait); @@ -3325,7 +3325,7 @@ int btrfs_sync_log(struct btrfs_trans_handle *trans, } ASSERT(log_transid == root->log_transid, "log_transid=%d root->log_transid=%d", log_transid, root->log_transid); - atomic_set(&root->log_commit[index1], 1); + root->log_commit[index1] = true; /* wait for previous tree log sync to complete */ wait_log_commit(root, log_transid - 1); @@ -3445,7 +3445,7 @@ int btrfs_sync_log(struct btrfs_trans_handle *trans, goto out; } - if (atomic_read(&log_root_tree->log_commit[index2])) { + if (log_root_tree->log_commit[index2]) { blk_finish_plug(&plug); ret = btrfs_wait_tree_log_extents(log, mark); wait_log_commit(log_root_tree, @@ -3459,7 +3459,7 @@ int btrfs_sync_log(struct btrfs_trans_handle *trans, ASSERT(root_log_ctx.log_transid == log_root_tree->log_transid, "root_log_ctx.log_transid=%d log_root_tree->log_transid=%d", root_log_ctx.log_transid, log_root_tree->log_transid); - atomic_set(&log_root_tree->log_commit[index2], 1); + log_root_tree->log_commit[index2] = true; wait_log_commit(log_root_tree, root_log_ctx.log_transid - 1); @@ -3559,7 +3559,7 @@ int btrfs_sync_log(struct btrfs_trans_handle *trans, /* * We know there can only be one task here, since we have not yet set - * root->log_commit[index1] to 0 and any task attempting to sync the + * root->log_commit[index1] to false and any task attempting to sync the * log must wait for the previous log transaction to commit if it's * still in progress or wait for the current log transaction commit if * someone else already started it. We use <= and not < because the @@ -3575,7 +3575,7 @@ int btrfs_sync_log(struct btrfs_trans_handle *trans, btrfs_remove_all_log_ctxs(log_root_tree, index2, ret); log_root_tree->log_transid_committed++; - atomic_set(&log_root_tree->log_commit[index2], 0); + log_root_tree->log_commit[index2] = false; mutex_unlock(&log_root_tree->log_mutex); /* @@ -3588,7 +3588,7 @@ int btrfs_sync_log(struct btrfs_trans_handle *trans, mutex_lock(&root->log_mutex); btrfs_remove_all_log_ctxs(root, index1, ret); root->log_transid_committed++; - atomic_set(&root->log_commit[index1], 0); + root->log_commit[index1] = false; mutex_unlock(&root->log_mutex); /* diff --git a/include/trace/events/btrfs.h b/include/trace/events/btrfs.h index 6c1438f6a4d3..6ecfab97c1a9 100644 --- a/include/trace/events/btrfs.h +++ b/include/trace/events/btrfs.h @@ -1613,9 +1613,9 @@ TRACE_EVENT(btrfs_sync_log_enter, __entry->log_transid_committed = data_race(root->log_transid_committed); __entry->log_committing = - atomic_read(&root->log_commit[ctx->log_transid % 2]); + data_race(root->log_commit[ctx->log_transid % 2]); __entry->log_committing_prev = - atomic_read(&root->log_commit[(ctx->log_transid + 1) % 2]); + data_race(root->log_commit[(ctx->log_transid + 1) % 2]); __entry->log_writers = atomic_read(&root->log_writers); ), From 4609c9276ab12022e55c1db42ef714e3ba801e5a Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Mon, 27 Jul 2026 08:49:46 +0930 Subject: [PATCH 58/72] btrfs: convert reflink.c to use btrfs_inode as parameters Inside reflink.c we still have a lot of functions passing VFS inode pointers, then internally convert them into btrfs_inode pointers. For example, inside btrfs_clone(), we have 12 BTRFS_I() call sites, while only 3 callsites that really require a VFS inode pointer. Do the cleanup to convert the following functions to pass a btrfs_inode pointer instead of a vanilla inode pointer: - btrfs_clone() - btrfs_extent_same_range() - clone_finish_inode_update(). Which covers all ad-hoc BTRFS_I() call sites inside reflink.c. Reviewed-by: Daniel Vacek Signed-off-by: Qu Wenruo Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/reflink.c | 101 ++++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 52 deletions(-) diff --git a/fs/btrfs/reflink.c b/fs/btrfs/reflink.c index 28bb05a92106..ec6a760519d9 100644 --- a/fs/btrfs/reflink.c +++ b/fs/btrfs/reflink.c @@ -20,30 +20,31 @@ #define BTRFS_MAX_DEDUPE_LEN SZ_16M static int clone_finish_inode_update(struct btrfs_trans_handle *trans, - struct inode *inode, + struct btrfs_inode *inode, u64 endoff, const u64 destoff, const u64 olen, bool no_time_update) { + struct inode *vfs_inode = &inode->vfs_inode; int ret; - inode_inc_iversion(inode); - if (!no_time_update) { - inode_set_mtime_to_ts(inode, inode_set_ctime_current(inode)); - } + inode_inc_iversion(vfs_inode); + if (!no_time_update) + inode_set_mtime_to_ts(vfs_inode, inode_set_ctime_current(vfs_inode)); + /* * We round up to the block size at eof when determining which * extents to clone above, but shouldn't round up the file size. */ if (endoff > destoff + olen) endoff = destoff + olen; - if (endoff > inode->i_size) { - i_size_write(inode, endoff); - btrfs_inode_safe_disk_i_size_write(BTRFS_I(inode), 0); + if (endoff > vfs_inode->i_size) { + i_size_write(vfs_inode, endoff); + btrfs_inode_safe_disk_i_size_write(inode, 0); } - ret = btrfs_update_inode(trans, BTRFS_I(inode)); + ret = btrfs_update_inode(trans, inode); if (unlikely(ret)) { btrfs_abort_transaction(trans, ret); btrfs_end_transaction(trans); @@ -392,11 +393,11 @@ static int clone_copy_inline_extent(struct btrfs_inode *inode, * @destoff: Offset within @inode to start clone * @no_time_update: Whether to update mtime/ctime on the target inode */ -static int btrfs_clone(struct inode *src, struct inode *inode, +static int btrfs_clone(struct btrfs_inode *src, struct btrfs_inode *inode, const u64 off, const u64 olen, const u64 olen_aligned, const u64 destoff, bool no_time_update) { - struct btrfs_fs_info *fs_info = inode_to_fs_info(inode); + struct btrfs_fs_info *fs_info = inode->root->fs_info; BTRFS_PATH_AUTO_FREE(path); struct extent_buffer *leaf; struct btrfs_trans_handle *trans; @@ -420,7 +421,7 @@ static int btrfs_clone(struct inode *src, struct inode *inode, path->reada = READA_FORWARD; /* Clone data */ - key.objectid = btrfs_ino(BTRFS_I(src)); + key.objectid = btrfs_ino(src); key.type = BTRFS_EXTENT_DATA_KEY; key.offset = off; @@ -436,8 +437,7 @@ static int btrfs_clone(struct inode *src, struct inode *inode, u64 drop_start; /* Note the key will change type as we walk through the tree */ - ret = btrfs_search_slot(NULL, BTRFS_I(src)->root, &key, path, - 0, 0); + ret = btrfs_search_slot(NULL, src->root, &key, path, 0, 0); if (ret < 0) goto out; /* @@ -455,7 +455,7 @@ static int btrfs_clone(struct inode *src, struct inode *inode, nritems = btrfs_header_nritems(path->nodes[0]); process_slot: if (path->slots[0] >= nritems) { - ret = btrfs_next_leaf(BTRFS_I(src)->root, path); + ret = btrfs_next_leaf(src->root, path); if (ret < 0) goto out; if (ret > 0) @@ -466,8 +466,7 @@ static int btrfs_clone(struct inode *src, struct inode *inode, slot = path->slots[0]; btrfs_item_key_to_cpu(leaf, &key, slot); - if (key.type > BTRFS_EXTENT_DATA_KEY || - key.objectid != btrfs_ino(BTRFS_I(src))) + if (key.type > BTRFS_EXTENT_DATA_KEY || key.objectid != btrfs_ino(src)) break; ASSERT(key.type == BTRFS_EXTENT_DATA_KEY, "key.type=%u", key.type); @@ -514,7 +513,7 @@ static int btrfs_clone(struct inode *src, struct inode *inode, btrfs_release_path(path); memcpy(&new_key, &key, sizeof(new_key)); - new_key.objectid = btrfs_ino(BTRFS_I(inode)); + new_key.objectid = btrfs_ino(inode); if (off <= key.offset) new_key.offset = key.offset + destoff - off; else @@ -558,7 +557,7 @@ static int btrfs_clone(struct inode *src, struct inode *inode, clone_info.extent_buf = buf; clone_info.is_new_extent = false; clone_info.update_times = !no_time_update; - ret = btrfs_replace_file_extents(BTRFS_I(inode), path, + ret = btrfs_replace_file_extents(inode, path, drop_start, new_key.offset + datal - 1, &clone_info, &trans); if (ret) @@ -582,7 +581,7 @@ static int btrfs_clone(struct inode *src, struct inode *inode, goto out; } - ret = clone_copy_inline_extent(BTRFS_I(inode), path, &new_key, + ret = clone_copy_inline_extent(inode, path, &new_key, drop_start, datal, size, comp, buf, &trans); if (ret) @@ -605,9 +604,9 @@ static int btrfs_clone(struct inode *src, struct inode *inode, * the checksums problem on fsync. */ if (extent_gen == trans->transid && disko > 0) - BTRFS_I(src)->last_reflink_trans = trans->transid; + src->last_reflink_trans = trans->transid; - BTRFS_I(inode)->last_reflink_trans = trans->transid; + inode->last_reflink_trans = trans->transid; last_dest_end = ALIGN(new_key.offset + datal, fs_info->sectorsize); @@ -653,10 +652,10 @@ static int btrfs_clone(struct inode *src, struct inode *inode, * set by previous calls to btrfs_replace_file_extents() that * replaced file extent items. */ - if (last_dest_end >= i_size_read(inode)) - btrfs_set_inode_full_sync(BTRFS_I(inode)); + if (last_dest_end >= i_size_read(&inode->vfs_inode)) + btrfs_set_inode_full_sync(inode); - ret = btrfs_replace_file_extents(BTRFS_I(inode), path, + ret = btrfs_replace_file_extents(inode, path, last_dest_end, destoff + len - 1, NULL, &trans); if (ret) goto out; @@ -666,7 +665,7 @@ static int btrfs_clone(struct inode *src, struct inode *inode, } out: - clear_bit(BTRFS_INODE_NO_DELALLOC_FLUSH, &BTRFS_I(inode)->runtime_flags); + clear_bit(BTRFS_INODE_NO_DELALLOC_FLUSH, &inode->runtime_flags); return ret; } @@ -701,8 +700,7 @@ static int btrfs_extent_same_range(struct btrfs_inode *src, u64 loff, u64 len, * mode. */ btrfs_lock_extent(&dst->io_tree, dst_loff, end, &cached_state); - ret = btrfs_clone(&src->vfs_inode, &dst->vfs_inode, loff, len, - ALIGN(len, bs), dst_loff, true); + ret = btrfs_clone(src, dst, loff, len, ALIGN(len, bs), dst_loff, true); btrfs_unlock_extent(&dst->io_tree, dst_loff, end, &cached_state); btrfs_btree_balance_dirty(fs_info); @@ -710,12 +708,12 @@ static int btrfs_extent_same_range(struct btrfs_inode *src, u64 loff, u64 len, return ret; } -static int btrfs_extent_same(struct inode *src, u64 loff, u64 olen, - struct inode *dst, u64 dst_loff) +static int btrfs_extent_same(struct btrfs_inode *src, u64 loff, u64 olen, + struct btrfs_inode *dst, u64 dst_loff) { int ret = 0; u64 i, tail_len, chunk_count; - struct btrfs_root *root_dst = BTRFS_I(dst)->root; + struct btrfs_root *root_dst = dst->root; spin_lock(&root_dst->root_item_lock); if (root_dst->send_in_progress) { @@ -733,8 +731,8 @@ static int btrfs_extent_same(struct inode *src, u64 loff, u64 olen, chunk_count = div_u64(olen, BTRFS_MAX_DEDUPE_LEN); for (i = 0; i < chunk_count; i++) { - ret = btrfs_extent_same_range(BTRFS_I(src), loff, BTRFS_MAX_DEDUPE_LEN, - BTRFS_I(dst), dst_loff); + ret = btrfs_extent_same_range(src, loff, BTRFS_MAX_DEDUPE_LEN, + dst, dst_loff); if (ret) goto out; @@ -743,8 +741,7 @@ static int btrfs_extent_same(struct inode *src, u64 loff, u64 olen, } if (tail_len > 0) - ret = btrfs_extent_same_range(BTRFS_I(src), loff, tail_len, - BTRFS_I(dst), dst_loff); + ret = btrfs_extent_same_range(src, loff, tail_len, dst, dst_loff); out: spin_lock(&root_dst->root_item_lock); root_dst->dedupe_in_progress--; @@ -757,9 +754,11 @@ static noinline int btrfs_clone_files(struct file *file, struct file *file_src, u64 off, u64 olen, u64 destoff) { struct extent_state *cached_state = NULL; - struct inode *inode = file_inode(file); - struct inode *src = file_inode(file_src); - struct btrfs_fs_info *fs_info = inode_to_fs_info(inode); + struct btrfs_inode *inode = BTRFS_I(file_inode(file)); + struct btrfs_inode *src = BTRFS_I(file_inode(file_src)); + struct btrfs_fs_info *fs_info = inode->root->fs_info; + const u64 src_isize = src->vfs_inode.i_size; + const u64 inode_isize = inode->vfs_inode.i_size; int ret; u64 len = olen; const u32 bs = fs_info->sectorsize; @@ -771,13 +770,13 @@ static noinline int btrfs_clone_files(struct file *file, struct file *file_src, * if the file size is not blocksize aligned. So we don't need to check * for that case here. */ - if (off + len == src->i_size) - len = ALIGN(src->i_size, bs) - off; + if (off + len == src_isize) + len = ALIGN(src_isize, bs) - off; - if (destoff > inode->i_size) { - const u64 wb_start = ALIGN_DOWN(inode->i_size, bs); + if (destoff > inode_isize) { + const u64 wb_start = ALIGN_DOWN(inode_isize, bs); - ret = btrfs_cont_expand(BTRFS_I(inode), inode->i_size, destoff); + ret = btrfs_cont_expand(inode, inode_isize, destoff); if (ret) return ret; /* @@ -789,8 +788,7 @@ static noinline int btrfs_clone_files(struct file *file, struct file *file_src, * we found the previous extent covering eof and before we * attempted to increment its reference count). */ - ret = btrfs_wait_ordered_range(BTRFS_I(inode), wb_start, - destoff - wb_start); + ret = btrfs_wait_ordered_range(inode, wb_start, destoff - wb_start); if (ret) return ret; } @@ -802,9 +800,9 @@ static noinline int btrfs_clone_files(struct file *file, struct file *file_src, * mode. */ end = destoff + len - 1; - btrfs_lock_extent(&BTRFS_I(inode)->io_tree, destoff, end, &cached_state); + btrfs_lock_extent(&inode->io_tree, destoff, end, &cached_state); ret = btrfs_clone(src, inode, off, olen, len, destoff, false); - btrfs_unlock_extent(&BTRFS_I(inode)->io_tree, destoff, end, &cached_state); + btrfs_unlock_extent(&inode->io_tree, destoff, end, &cached_state); if (ret < 0) return ret; @@ -818,7 +816,7 @@ static noinline int btrfs_clone_files(struct file *file, struct file *file_src, * could come from some range other than the copied inline extent's * destination range and we have no way to know that. */ - ret = btrfs_wait_ordered_range(BTRFS_I(inode), destoff, len); + ret = btrfs_wait_ordered_range(inode, destoff, len); if (ret < 0) return ret; @@ -826,7 +824,7 @@ static noinline int btrfs_clone_files(struct file *file, struct file *file_src, * Invalidate page cache so that future reads will see the cloned data * immediately and not the previous data. */ - ret = filemap_invalidate_inode(inode, false, destoff, end); + ret = filemap_invalidate_inode(&inode->vfs_inode, false, destoff, end); if (ret < 0) return ret; @@ -934,7 +932,7 @@ loff_t btrfs_remap_file_range(struct file *src_file, loff_t off, bool same_inode = dst_inode == src_inode; int ret; - if (btrfs_is_shutdown(inode_to_fs_info(file_inode(src_file)))) + if (btrfs_is_shutdown(src_inode->root->fs_info)) return -EIO; if (remap_flags & ~(REMAP_FILE_DEDUP | REMAP_FILE_ADVISORY)) @@ -953,8 +951,7 @@ loff_t btrfs_remap_file_range(struct file *src_file, loff_t off, goto out_unlock; if (remap_flags & REMAP_FILE_DEDUP) - ret = btrfs_extent_same(&src_inode->vfs_inode, off, len, - &dst_inode->vfs_inode, destoff); + ret = btrfs_extent_same(src_inode, off, len, dst_inode, destoff); else ret = btrfs_clone_files(dst_file, src_file, off, len, destoff); From ecc05eda9a346848ae01a6c8bfa3f0bec133bd8b Mon Sep 17 00:00:00 2001 From: Johannes Thumshirn Date: Thu, 23 Jul 2026 17:54:25 +0200 Subject: [PATCH 59/72] btrfs: zoned: flush active metadata block group at btree_writepages() start btree_writepages() writes the btree inode's dirty metadata in ascending logical address order. On a zoned filesystem only one metadata and one system block group is active for writing at a time, and check_bg_is_active() (via btrfs_check_meta_write_pointer()) pivots the active block group as writeback moves from one block group to the next. If the active block group sits at a higher logical address than another block group that also holds dirty metadata, the ascending walk reaches the lower one first and, to write it, has to finish the active block group and activate the lower one. It cannot finish a block group that still has unsent IO, and during WB_SYNC_ALL && !for_sync (commit) writeback it deliberately refuses to wait for that IO under fs_info->zoned_meta_io_lock, as that can deadlock. The pivot thus cannot issue the submission itself either, so it gives up: btrfs_check_meta_write_pointer() returns -EAGAIN, which btrfs_write_and_wait_transaction() treats as fatal and aborts the transaction, forcing the filesystem read-only. This happens intermittently under metadata-heavy relocation (e.g. fstests btrfs/187). Flush the active metadata and system block groups at the start of btree_writepages(), under the fs_info->zoned_meta_io_lock it already holds, so they have no unsent IO left and the later pivot can finish them and make forward progress. Fixes: 13bb483d32ab ("btrfs: zoned: activate metadata block group on write time") Assisted-by: LLM (debugging, commit message) Reviewed-by: Boris Burkov Signed-off-by: Johannes Thumshirn Signed-off-by: David Sterba --- fs/btrfs/extent_io.c | 113 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 92 insertions(+), 21 deletions(-) diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c index 632637c49732..585169ad98ba 100644 --- a/fs/btrfs/extent_io.c +++ b/fs/btrfs/extent_io.c @@ -2496,6 +2496,76 @@ void btrfs_btree_wait_writeback_range(struct btrfs_fs_info *fs_info, u64 start, } } +static int write_meta_extent_buffer(struct btrfs_eb_write_context *ctx, + struct writeback_control *wbc) +{ + struct extent_buffer *eb = ctx->eb; + int ret; + + ret = btrfs_check_meta_write_pointer(eb->fs_info, ctx); + if (ret) + return ret; + + if (!lock_extent_buffer_for_io(eb, wbc)) + return 0; + + /* Implies write in zoned mode. */ + if (ctx->zoned_bg) { + /* Mark the last eb in the block group. */ + btrfs_schedule_zone_finish_bg(ctx->zoned_bg, eb); + ctx->zoned_bg->meta_write_pointer += eb->len; + } + write_one_eb(eb, wbc); + return 0; +} + +/* + * On a zoned filesystem, write out the currently dirty metadata extent buffers + * of @bg. Used to flush the active metadata/system block group before the + * ascending-address walk in btree_writepages(), so that walk can pivot the + * active block group away (finishing it) instead of aborting the commit; see + * the caller for details. + */ +static void flush_active_meta_bg(struct address_space *mapping, + struct writeback_control *wbc, + struct btrfs_eb_write_context *ctx, + struct btrfs_block_group *bg) +{ + struct btrfs_fs_info *fs_info = inode_to_fs_info(mapping->host); + unsigned long index = bg->start >> fs_info->nodesize_bits; + unsigned long end = (btrfs_block_group_end(bg) - 1) >> fs_info->nodesize_bits; + struct eb_batch batch; + unsigned int nr_ebs; + + ASSERT(btrfs_is_zoned(fs_info)); + lockdep_assert_held(&fs_info->zoned_meta_io_lock); + + eb_batch_init(&batch); + while (index <= end && + (nr_ebs = buffer_tree_get_ebs_tag(fs_info, &index, end, + PAGECACHE_TAG_DIRTY, &batch))) { + struct extent_buffer *eb; + + while ((eb = eb_batch_next(&batch)) != NULL) { + ctx->eb = eb; + + /* + * If the eb is behind the write pointer (-EBUSY, e.g. + * already being written by someone else) skip it and + * carry on. Only a hole at the write pointer (-EAGAIN) + * stops the flush. The main walk in btree_writepages() + * then deals with it. + */ + if (write_meta_extent_buffer(ctx, wbc) == -EAGAIN) { + eb_batch_release(&batch); + return; + } + } + eb_batch_release(&batch); + cond_resched(); + } +} + int btree_writepages(struct address_space *mapping, struct writeback_control *wbc) { struct btrfs_eb_write_context ctx = { .wbc = wbc }; @@ -2531,6 +2601,22 @@ int btree_writepages(struct address_space *mapping, struct writeback_control *wb else tag = PAGECACHE_TAG_DIRTY; btrfs_zoned_meta_io_lock(fs_info); + + /* + * On a zoned filesystem, flush the currently active metadata/system + * block group(s) first, under this same lock, so the ascending-address + * walk below can pivot the active block group instead of aborting the + * transaction commit with -EAGAIN. + */ + if (btrfs_is_zoned(fs_info) && wbc->sync_mode == WB_SYNC_ALL && + !wbc->for_sync) { + if (fs_info->active_meta_bg) + flush_active_meta_bg(mapping, wbc, &ctx, + fs_info->active_meta_bg); + if (fs_info->active_system_bg) + flush_active_meta_bg(mapping, wbc, &ctx, + fs_info->active_system_bg); + } retry: if (wbc->sync_mode == WB_SYNC_ALL) buffer_tree_tag_for_writeback(fs_info, index, end); @@ -2541,28 +2627,13 @@ int btree_writepages(struct address_space *mapping, struct writeback_control *wb while ((eb = eb_batch_next(&batch)) != NULL) { ctx.eb = eb; - ret = btrfs_check_meta_write_pointer(eb->fs_info, &ctx); - if (ret) { - if (ret == -EBUSY) - ret = 0; - - if (ret) { - done = true; - break; - } - continue; + ret = write_meta_extent_buffer(&ctx, wbc); + if (ret == -EBUSY) { + ret = 0; + } else if (ret) { + done = true; + break; } - - if (!lock_extent_buffer_for_io(eb, wbc)) - continue; - - /* Implies write in zoned mode. */ - if (ctx.zoned_bg) { - /* Mark the last eb in the block group. */ - btrfs_schedule_zone_finish_bg(ctx.zoned_bg, eb); - ctx.zoned_bg->meta_write_pointer += eb->len; - } - write_one_eb(eb, wbc); } nr_to_write_done = (wbc->nr_to_write <= 0); eb_batch_release(&batch); From 3cbcc099b4d42667ea8c6b10d28f6060c104048c Mon Sep 17 00:00:00 2001 From: Johannes Thumshirn Date: Thu, 23 Jul 2026 17:54:26 +0200 Subject: [PATCH 60/72] btrfs: zoned: drop stranded dirty metadata on transaction abort On a zoned filesystem a freed tree block is not cleared but kept dirty and flagged EXTENT_BUFFER_ZONED_ZEROOUT, so a later writeback zeroes it out and advances the zone write pointer. A transaction abort turns the filesystem read-only before that writeback runs, so these buffers stay dirty and stranded ahead of the write pointer where btree_writepages() can no longer write them. They survive to the final iput() of the btree inode at unmount, which submits the write after the endio workqueues are gone, hanging unmount in folio_wait_writeback(). Clear the dirty state of such buffers when cleaning up the aborted transaction, where the buffer tree still references all of them. Assisted-by: LLM (debugging, commit message) Reviewed-by: Boris Burkov Signed-off-by: Johannes Thumshirn Signed-off-by: David Sterba --- fs/btrfs/disk-io.c | 1 + fs/btrfs/extent_io.c | 74 ++++++++++++++++++++++++++++++++++---------- fs/btrfs/extent_io.h | 1 + 3 files changed, 60 insertions(+), 16 deletions(-) diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c index e7433294906e..6cf147242506 100644 --- a/fs/btrfs/disk-io.c +++ b/fs/btrfs/disk-io.c @@ -5020,6 +5020,7 @@ static int btrfs_cleanup_transaction(struct btrfs_fs_info *fs_info) btrfs_assert_delayed_root_empty(fs_info); btrfs_destroy_all_delalloc_inodes(fs_info); btrfs_drop_all_logs(fs_info); + btrfs_zoned_release_dirty_metadata(fs_info); btrfs_free_all_qgroup_pertrans(fs_info); mutex_unlock(&fs_info->transaction_kthread_mutex); diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c index 585169ad98ba..52da6e544c11 100644 --- a/fs/btrfs/extent_io.c +++ b/fs/btrfs/extent_io.c @@ -4024,6 +4024,32 @@ void free_extent_buffer_stale(struct extent_buffer *eb) release_extent_buffer(eb); } +static void clear_extent_buffer_dirty(struct extent_buffer *eb) +{ + struct btrfs_fs_info *fs_info = eb->fs_info; + + if (!test_and_clear_bit(EXTENT_BUFFER_DIRTY, &eb->bflags)) + return; + + buffer_tree_clear_mark(eb, PAGECACHE_TAG_DIRTY); + percpu_counter_add_batch(&fs_info->dirty_metadata_bytes, -(s64)eb->len, + fs_info->dirty_metadata_batch); + + for (int i = 0; i < num_extent_folios(eb); i++) { + struct folio *folio = eb->folios[i]; + bool last; + + if (!folio_test_dirty(folio)) + continue; + folio_lock(folio); + last = btrfs_meta_folio_clear_and_test_dirty(folio, eb); + if (last) + btrfs_clear_folio_dirty_tag(folio); + folio_unlock(folio); + } + WARN_ON(refcount_read(&eb->refs) == 0); +} + void btrfs_clear_buffer_dirty(struct btrfs_trans_handle *trans, struct extent_buffer *eb) { @@ -4048,26 +4074,42 @@ void btrfs_clear_buffer_dirty(struct btrfs_trans_handle *trans, return; } - if (!test_and_clear_bit(EXTENT_BUFFER_DIRTY, &eb->bflags)) + clear_extent_buffer_dirty(eb); +} + +/* + * On a zoned filesystem a freed tree block is kept dirty and flagged as + * EXTENT_BUFFER_ZONED_ZEROOUT so a later writeback zeroes it out and advances + * the zone write pointer. Such buffers still dirty when the filesystem is torn + * down can no longer be written back and are stale; if left dirty they hang the + * final iput() of the btree inode. Drop their dirty state, and the deferred + * zero-out along with it. + */ +void btrfs_zoned_release_dirty_metadata(struct btrfs_fs_info *fs_info) +{ + struct eb_batch batch; + unsigned long index = 0; + + if (!btrfs_is_zoned(fs_info)) return; - buffer_tree_clear_mark(eb, PAGECACHE_TAG_DIRTY); - percpu_counter_add_batch(&fs_info->dirty_metadata_bytes, -(s64)eb->len, - fs_info->dirty_metadata_batch); + btrfs_zoned_meta_io_lock(fs_info); + eb_batch_init(&batch); + while (buffer_tree_get_ebs_tag(fs_info, &index, ULONG_MAX, + PAGECACHE_TAG_DIRTY, &batch)) { + struct extent_buffer *eb; - for (int i = 0; i < num_extent_folios(eb); i++) { - struct folio *folio = eb->folios[i]; - bool last; - - if (!folio_test_dirty(folio)) - continue; - folio_lock(folio); - last = btrfs_meta_folio_clear_and_test_dirty(folio, eb); - if (last) - btrfs_clear_folio_dirty_tag(folio); - folio_unlock(folio); + while ((eb = eb_batch_next(&batch)) != NULL) { + btrfs_tree_lock(eb); + if (test_and_clear_bit(EXTENT_BUFFER_ZONED_ZEROOUT, + &eb->bflags)) + clear_extent_buffer_dirty(eb); + btrfs_tree_unlock(eb); + } + eb_batch_release(&batch); + cond_resched(); } - WARN_ON(refcount_read(&eb->refs) == 0); + btrfs_zoned_meta_io_unlock(fs_info); } void set_extent_buffer_dirty(struct extent_buffer *eb) diff --git a/fs/btrfs/extent_io.h b/fs/btrfs/extent_io.h index 869925337699..ad4ffce32702 100644 --- a/fs/btrfs/extent_io.h +++ b/fs/btrfs/extent_io.h @@ -393,6 +393,7 @@ void extent_clear_unlock_delalloc(struct btrfs_inode *inode, u64 start, u64 end, u32 bits_to_clear, unsigned long page_ops); void btrfs_clear_buffer_dirty(struct btrfs_trans_handle *trans, struct extent_buffer *buf); +void btrfs_zoned_release_dirty_metadata(struct btrfs_fs_info *fs_info); static inline void btrfs_clear_folio_dirty_tag(struct folio *folio) { From 7636e0b45c1d358388fe66728299bb5d45d544aa Mon Sep 17 00:00:00 2001 From: Johannes Thumshirn Date: Thu, 23 Jul 2026 17:54:27 +0200 Subject: [PATCH 61/72] btrfs: zoned: drop stranded dirty metadata buffers at unmount On a zoned filesystem a freed tree block is kept dirty and flagged EXTENT_BUFFER_ZONED_ZEROOUT so a later writeback zeroes it out and advances the zone write pointer. Unsynced tree-log updates (e.g. from rename or link) leave such buffers behind when the log is freed at commit, and across log generations they can end up ahead of the write pointer behind a hole, so btree_writepages() can never write them. During normal operation the space is later reclaimed by a zone reset; at unmount it is not, and the buffers survive to the final iput() of the btree inode, which hangs in folio_wait_writeback() once the endio workqueues are stopped. They cannot be written back from where they are freed (free_log_tree(), inside the committing transaction) without deadlocking against that commit, and they are stale anyway, not referenced by the committed superblock. Drop their dirty state in close_ctree(), before btrfs_stop_all_workers(). Signed-off-by: Johannes Thumshirn Signed-off-by: David Sterba --- fs/btrfs/disk-io.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c index 6cf147242506..a8e2c15f6823 100644 --- a/fs/btrfs/disk-io.c +++ b/fs/btrfs/disk-io.c @@ -4572,6 +4572,13 @@ void __cold close_ctree(struct btrfs_fs_info *fs_info) free_root_pointers(fs_info, true); btrfs_free_fs_roots(fs_info); + /* + * Drop metadata left stranded ahead of a zone write pointer while the + * endio workqueues are still up, so the final iput() of the btree inode + * below does not hang submitting a write that can no longer complete. + */ + btrfs_zoned_release_dirty_metadata(fs_info); + /* * We must make sure there is not any read request to * submit after we stop all workers. From db4b9eefc8ee0bcaeee4d5e6a7313905f6a2fe7c Mon Sep 17 00:00:00 2001 From: Johannes Thumshirn Date: Thu, 23 Jul 2026 17:54:28 +0200 Subject: [PATCH 62/72] btrfs: zoned: don't clobber the extent buffer when zeroing it out On a zoned filesystem a freed-but-still-dirty tree block is written out as zeros (EXTENT_BUFFER_ZONED_ZEROOUT) only to keep the zone write pointer advancing. btree_csum_one_bio() implemented this by memzeroing the extent buffer's own folios before submission. That destroys the in-memory buffer while it may still be referenced. In particular btrfs_free_tree_block() can run on it afterwards and reads the header to add a delayed reference; once the header has been zeroed it frees bytenr 0 and corrupts the extent tree (the btrfs_header_bytenr(buf) != 0 ASSERT in btrfs_free_tree_block(), or an "unable to find ref" abort). It is flaky and reproduces under fsstress, e.g. generic/461 and generic/013. Write the zeros to disk from the shared zero page instead and leave the extent buffer content untouched, so any later reference - including the delayed reference from btrfs_free_tree_block() - still sees a valid header. end_bbio_meta_write() now clears writeback on the buffer's own folios, as the bio no longer carries them. Fixes: aa6313e6ff2b ("btrfs: zoned: don't clear dirty flag of extent buffer") Assisted-by: LLM (debugging, commit message) Reviewed-by: Boris Burkov Signed-off-by: Johannes Thumshirn Signed-off-by: David Sterba --- fs/btrfs/disk-io.c | 13 +++++++------ fs/btrfs/extent_io.c | 31 ++++++++++++++++++++++++------- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c index a8e2c15f6823..cc4dcd10631a 100644 --- a/fs/btrfs/disk-io.c +++ b/fs/btrfs/disk-io.c @@ -271,14 +271,15 @@ int btree_csum_one_bio(struct btrfs_bio *bbio) return -EIO; /* - * If an extent_buffer is marked as EXTENT_BUFFER_ZONED_ZEROOUT, don't - * checksum it but zero-out its content. This is done to preserve - * ordering of I/O without unnecessarily writing out data. + * An extent_buffer marked EXTENT_BUFFER_ZONED_ZEROOUT is written out as + * zeros to preserve ordering of I/O without persisting the now + * unnecessary block. The bio is fed from the shared zero page (see + * write_one_eb()), so there is nothing to checksum here. Crucially, the + * buffer's own content is left intact: it may still be referenced, e.g. + * btrfs_free_tree_block() reads its header to add a delayed reference. */ - if (test_bit(EXTENT_BUFFER_ZONED_ZEROOUT, &eb->bflags)) { - memzero_extent_buffer(eb, 0, eb->len); + if (test_bit(EXTENT_BUFFER_ZONED_ZEROOUT, &eb->bflags)) return 0; - } if (WARN_ON_ONCE(found_start != eb->start)) return -EIO; diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c index 52da6e544c11..c7c3f138fb69 100644 --- a/fs/btrfs/extent_io.c +++ b/fs/btrfs/extent_io.c @@ -2379,14 +2379,17 @@ static struct extent_buffer *find_extent_buffer_nolock( static void end_bbio_meta_write(struct btrfs_bio *bbio) { struct extent_buffer *eb = bbio->private; - struct folio_iter fi; if (bbio->bio.bi_status != BLK_STS_OK) set_btree_ioerr(eb); - bio_for_each_folio_all(fi, &bbio->bio) { - btrfs_meta_folio_clear_writeback(fi.folio, eb); - } + /* + * Clear writeback on the buffer's own folios. The bio may carry the + * shared zero page instead (EXTENT_BUFFER_ZONED_ZEROOUT), so iterate + * the extent buffer folios rather than the bio folios. + */ + for (int i = 0; i < num_extent_folios(eb); i++) + btrfs_meta_folio_clear_writeback(eb->folios[i], eb); buffer_tree_clear_mark(eb, PAGECACHE_TAG_WRITEBACK); clear_and_wake_up_bit(EXTENT_BUFFER_WRITEBACK, &eb->bflags); @@ -2427,7 +2430,8 @@ static noinline_for_stack void write_one_eb(struct extent_buffer *eb, struct btrfs_fs_info *fs_info = eb->fs_info; struct btrfs_bio *bbio; - prepare_eb_write(eb); + if (!test_bit(EXTENT_BUFFER_ZONED_ZEROOUT, &eb->bflags)) + prepare_eb_write(eb); bbio = btrfs_bio_alloc(INLINE_EXTENT_BUFFER_PAGES, REQ_OP_WRITE | REQ_META | wbc_to_write_flags(wbc), @@ -2447,8 +2451,21 @@ static noinline_for_stack void write_one_eb(struct extent_buffer *eb, btrfs_meta_folio_set_writeback(folio, eb); if (!folio_test_dirty(folio)) wbc->nr_to_write -= folio_nr_pages(folio); - bio_add_folio_nofail(&bbio->bio, folio, range_len, - offset_in_folio(folio, range_start)); + if (test_bit(EXTENT_BUFFER_ZONED_ZEROOUT, &eb->bflags)) { + u32 off = 0; + + while (off < range_len) { + u32 add = min_t(u32, PAGE_SIZE, range_len - off); + + bio_add_folio_nofail(&bbio->bio, + page_folio(ZERO_PAGE(0)), + add, 0); + off += add; + } + } else { + bio_add_folio_nofail(&bbio->bio, folio, range_len, + offset_in_folio(folio, range_start)); + } wbc_account_cgroup_owner(wbc, folio, range_len); folio_unlock(folio); } From 681e073614515b892cacef0eeec0c761a2c2ab87 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Tue, 28 Jul 2026 19:34:09 +0930 Subject: [PATCH 63/72] btrfs: use aligned range for locking in extent_fiemap() The @end parameter for all extent io tree helpers is inclusive, but the call site in extent_fiemap() is passing an exclusive end into btrfs_lock_extent(), which will step into the next block unexpectedly. Pass the inclusive end into btrfs_lock_extent() and btrfs_unlock_extent(). Fixes: ac3c0d36a2a2 ("btrfs: make fiemap more efficient and accurate reporting extent sharedness") Reviewed-by: Filipe Manana Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/fiemap.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/btrfs/fiemap.c b/fs/btrfs/fiemap.c index ba6a360074c0..7a2a97180099 100644 --- a/fs/btrfs/fiemap.c +++ b/fs/btrfs/fiemap.c @@ -660,7 +660,7 @@ static int extent_fiemap(struct btrfs_inode *inode, range_end = round_up(start + len, sectorsize); prev_extent_end = range_start; - btrfs_lock_extent(&inode->io_tree, range_start, range_end, &cached_state); + btrfs_lock_extent(&inode->io_tree, range_start, range_end - 1, &cached_state); ret = fiemap_find_last_extent_offset(inode, path, &last_extent_end); if (ret < 0) @@ -840,7 +840,7 @@ static int extent_fiemap(struct btrfs_inode *inode, } out_unlock: - btrfs_unlock_extent(&inode->io_tree, range_start, range_end, &cached_state); + btrfs_unlock_extent(&inode->io_tree, range_start, range_end - 1, &cached_state); if (ret == BTRFS_FIEMAP_FLUSH_CACHE) { btrfs_release_path(path); From e7f4c05a8307701da8bc8d0391540adf6c13fcfe Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Tue, 28 Jul 2026 18:41:23 +0930 Subject: [PATCH 64/72] btrfs: use aligned range for locking in reflink In btrfs_extent_same_range() and btrfs_clone_files(), the range passed into btrfs_lock_extent() is not aligned at its end, because we can reflink until the EOF, which may not be block aligned. Although this is not a big deal, for the sake of consistency, and to prepare for the upcoming stricter alignment check, pass an aligned range end to btrfs_lock_extent() and btrfs_unlock_extent(). Reviewed-by: Filipe Manana Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/reflink.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fs/btrfs/reflink.c b/fs/btrfs/reflink.c index ec6a760519d9..d2a4101912bd 100644 --- a/fs/btrfs/reflink.c +++ b/fs/btrfs/reflink.c @@ -687,10 +687,10 @@ static void btrfs_double_mmap_unlock(struct btrfs_inode *inode1, struct btrfs_in static int btrfs_extent_same_range(struct btrfs_inode *src, u64 loff, u64 len, struct btrfs_inode *dst, u64 dst_loff) { - const u64 end = dst_loff + len - 1; struct extent_state *cached_state = NULL; struct btrfs_fs_info *fs_info = src->root->fs_info; const u32 bs = fs_info->sectorsize; + const u64 end = round_up(dst_loff + len, bs) - 1; int ret; /* @@ -799,7 +799,7 @@ static noinline int btrfs_clone_files(struct file *file, struct file *file_src, * because we have already locked the inode's i_mmap_lock in exclusive * mode. */ - end = destoff + len - 1; + end = round_up(destoff + len, bs) - 1; btrfs_lock_extent(&inode->io_tree, destoff, end, &cached_state); ret = btrfs_clone(src, inode, off, olen, len, destoff, false); btrfs_unlock_extent(&inode->io_tree, destoff, end, &cached_state); From e0e6df7294101e22381e5b351f1e42f511327758 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Tue, 28 Jul 2026 18:41:24 +0930 Subject: [PATCH 65/72] btrfs: add validation for extent states Extent maps have the extra validation since commit 3f255ece2f1e ("btrfs: introduce extra sanity checks for extent maps"), but extent states do not have a similar check. Introduce a basic alignment check for the following call sites, so that we can cover all extent states inserted into the tree: - insert_state_fast() - insert_state() - split_state() Reviewed-by: Filipe Manana Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/extent-io-tree.c | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/fs/btrfs/extent-io-tree.c b/fs/btrfs/extent-io-tree.c index c18ea5ef2974..d6df11f6088c 100644 --- a/fs/btrfs/extent-io-tree.c +++ b/fs/btrfs/extent-io-tree.c @@ -334,6 +334,21 @@ static inline struct extent_state *tree_search(struct extent_io_tree *tree, u64 return tree_search_for_insert(tree, offset, NULL, NULL); } +static void validate_extent_state(const struct extent_io_tree *tree, + const struct extent_state *state) +{ + u32 blocksize; + + if (tree->owner != IO_TREE_INODE_IO) + return; + + blocksize = btrfs_extent_io_tree_to_fs_info(tree)->sectorsize; + ASSERT(IS_ALIGNED(state->start, blocksize) && + IS_ALIGNED(state->end + 1, blocksize), + "unaligned extent state, blocksize=%u start=%llu end=%llu state=0x%x", + blocksize, state->start, state->end, state->state); +} + #define extent_io_tree_panic(tree, state, opname, err) \ btrfs_panic(btrfs_extent_io_tree_to_fs_info((tree)), (err), \ "extent io tree error on %s state start %llu end %llu", \ @@ -429,6 +444,8 @@ static struct extent_state *insert_state(struct extent_io_tree *tree, const u64 end = state->end + 1; const bool try_merge = !(bits & (EXTENT_LOCK_BITS | EXTENT_BOUNDARY)); + validate_extent_state(tree, state); + set_state_bits(tree, state, bits, changeset); node = &tree->state.rb_node; @@ -481,6 +498,8 @@ static void insert_state_fast(struct extent_io_tree *tree, struct rb_node *parent, unsigned bits, struct extent_changeset *changeset) { + validate_extent_state(tree, state); + set_state_bits(tree, state, bits, changeset); rb_link_node(&state->rb_node, parent, node); rb_insert_color(&state->rb_node, &tree->state); @@ -533,6 +552,8 @@ static int split_state(struct extent_io_tree *tree, struct extent_state *orig, } } + validate_extent_state(tree, orig); + validate_extent_state(tree, prealloc); rb_link_node(&prealloc->rb_node, parent, node); rb_insert_color(&prealloc->rb_node, &tree->state); From 9102b179512e11644fb0489ae62010a09afa199c Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Tue, 28 Jul 2026 12:09:26 +0930 Subject: [PATCH 66/72] btrfs: qgroup: fix a wrong length calculation in qgroup_free_reserved_data() In that function, we round down the start position and round up the ending position. But during the calculation of @len, we use "round_up(start + len, sectorsize)", which is the rounded up end position, not the rounded up length. Which results a much larger length, and later we are still using "start + len", which is completely incorrect. Fix it by declaring a local @aligned_start and @aligned_len and use them instead. Fixes: bc42bda22345 ("btrfs: qgroup: Fix qgroup reserved space underflow by only freeing reserved ranges") Reviewed-by: Filipe Manana Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/qgroup.c | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/fs/btrfs/qgroup.c b/fs/btrfs/qgroup.c index 210af4d7d4b5..f68b696b4bf7 100644 --- a/fs/btrfs/qgroup.c +++ b/fs/btrfs/qgroup.c @@ -4339,12 +4339,13 @@ static int qgroup_free_reserved_data(struct btrfs_inode *inode, struct ulist_node *unode; struct ulist_iterator uiter; struct extent_changeset changeset; + const u32 sectorsize = root->fs_info->sectorsize; + const u64 aligned_start = round_down(start, sectorsize); + const u64 aligned_len = round_up(start + len, sectorsize) - aligned_start; u64 freed = 0; int ret; extent_changeset_init_bytes_only(&changeset); - len = round_up(start + len, root->fs_info->sectorsize); - start = round_down(start, root->fs_info->sectorsize); ULIST_ITER_INIT(&uiter); while ((unode = ulist_next(&reserved->range_changed, &uiter))) { @@ -4356,12 +4357,15 @@ static int qgroup_free_reserved_data(struct btrfs_inode *inode, extent_changeset_release(&changeset); - /* Only free range in range [start, start + len) */ - if (range_start >= start + len || - range_start + range_len <= start) + /* + * Only free the range within + * [aligned_start, aligned_start + aligned_len). + */ + if (range_start >= aligned_start + aligned_len || + range_start + range_len <= aligned_start) continue; - free_start = max(range_start, start); - free_len = min(start + len, range_start + range_len) - + free_start = max(range_start, aligned_start); + free_len = min(aligned_start + aligned_len, range_start + range_len) - free_start; /* * TODO: To also modify reserved->ranges_reserved to reflect From e8e7aff88e5bd35da940e249dde14393acdee98e Mon Sep 17 00:00:00 2001 From: Boris Burkov Date: Tue, 21 Jul 2026 15:42:12 -0700 Subject: [PATCH 67/72] btrfs: factor init_extent_buffer from __alloc_extent_buffer In preparation for preallocating extent_buffer data, factor eb initialization away from specifically allocating it. This allows us to allocate the eb, bfs, folios, etc. together in the main search_slot code paths, but still share initialization code with the dummy/test/clone allocation paths. Reviewed-by: Filipe Manana Signed-off-by: Boris Burkov Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/extent_io.c | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c index c7c3f138fb69..780939756f24 100644 --- a/fs/btrfs/extent_io.c +++ b/fs/btrfs/extent_io.c @@ -3309,12 +3309,9 @@ void btrfs_uninhibit_all_eb_writeback(struct btrfs_trans_handle *trans) trans->inhibited_ebs_hand = 0; } -static struct extent_buffer *__alloc_extent_buffer(struct btrfs_fs_info *fs_info, - u64 start) +static void init_extent_buffer(struct btrfs_fs_info *fs_info, + struct extent_buffer *eb, u64 start) { - struct extent_buffer *eb = NULL; - - eb = kmem_cache_zalloc(extent_buffer_cache, GFP_NOFS|__GFP_NOFAIL); eb->start = start; eb->len = fs_info->nodesize; eb->fs_info = fs_info; @@ -3327,7 +3324,15 @@ static struct extent_buffer *__alloc_extent_buffer(struct btrfs_fs_info *fs_info refcount_set(&eb->refs, 1); ASSERT(eb->len <= BTRFS_MAX_METADATA_BLOCKSIZE); +} +static struct extent_buffer *__alloc_extent_buffer(struct btrfs_fs_info *fs_info, + u64 start) +{ + struct extent_buffer *eb; + + eb = kmem_cache_zalloc(extent_buffer_cache, GFP_NOFS | __GFP_NOFAIL); + init_extent_buffer(fs_info, eb, start); return eb; } @@ -3731,9 +3736,8 @@ struct extent_buffer *alloc_extent_buffer(struct btrfs_fs_info *fs_info, if (eb) return eb; - eb = __alloc_extent_buffer(fs_info, start); - if (!eb) - return ERR_PTR(-ENOMEM); + eb = kmem_cache_zalloc(extent_buffer_cache, GFP_NOFS | __GFP_NOFAIL); + init_extent_buffer(fs_info, eb, start); /* * The reloc trees are just snapshots, so we need them to appear to be From 368f20e65afa4f3a39b7166f68832993ff75b48c Mon Sep 17 00:00:00 2001 From: Boris Burkov Date: Tue, 21 Jul 2026 15:42:13 -0700 Subject: [PATCH 68/72] btrfs: add struct btrfs_eb_prealloc In further preparation for supporting NOFAIL allocations with retries outside the critical section, add a struct to carry the extent_buffer and btrfs_folio_state we need to allocate. Refactor the allocation pathways to use the new struct but with no functional change. Wire empty prealloc structs in from callers. Reviewed-by: Filipe Manana Reviewed-by: Jeff Layton Signed-off-by: Boris Burkov Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/ctree.c | 21 +++-- fs/btrfs/disk-io.c | 6 +- fs/btrfs/disk-io.h | 2 + fs/btrfs/extent-tree.c | 6 +- fs/btrfs/extent_io.c | 176 ++++++++++++++++++++++++++++------------- fs/btrfs/extent_io.h | 19 +++++ fs/btrfs/tree-log.c | 3 +- 7 files changed, 166 insertions(+), 67 deletions(-) diff --git a/fs/btrfs/ctree.c b/fs/btrfs/ctree.c index 49fb6b816aa9..261ef4ec7d1b 100644 --- a/fs/btrfs/ctree.c +++ b/fs/btrfs/ctree.c @@ -1460,6 +1460,7 @@ static noinline void unlock_up(struct btrfs_path *path, int level, */ static int read_block_for_search(struct btrfs_root *root, struct btrfs_path *p, + struct btrfs_eb_prealloc *pa, struct extent_buffer **eb_ret, int slot, const struct btrfs_key *key) { @@ -1546,7 +1547,8 @@ read_block_for_search(struct btrfs_root *root, struct btrfs_path *p, if (p->reada != READA_NONE) reada_for_search(fs_info, p, parent_level, slot, key->objectid); - tmp = btrfs_find_create_tree_block(fs_info, blocknr, check.owner_root, check.level); + tmp = btrfs_find_create_tree_block(fs_info, pa, blocknr, + check.owner_root, check.level); if (IS_ERR(tmp)) { ret = PTR_ERR(tmp); tmp = NULL; @@ -2004,6 +2006,7 @@ int btrfs_search_slot(struct btrfs_trans_handle *trans, struct btrfs_root *root, u8 lowest_level = 0; int min_write_lock_level; int prev_cmp; + struct btrfs_eb_prealloc pa = { 0 }; if (!root) return -EINVAL; @@ -2187,7 +2190,7 @@ int btrfs_search_slot(struct btrfs_trans_handle *trans, struct btrfs_root *root, goto done; } - ret2 = read_block_for_search(root, p, &b, slot, key); + ret2 = read_block_for_search(root, p, &pa, &b, slot, key); if (ret2 == -EAGAIN && !p->nowait) { trace_btrfs_search_slot_restart(root, level, "read_block"); goto again; @@ -2234,6 +2237,8 @@ int btrfs_search_slot(struct btrfs_trans_handle *trans, struct btrfs_root *root, ret = ret2; } + btrfs_free_eb_prealloc(&pa); + return ret; } ALLOW_ERROR_INJECTION(btrfs_search_slot, ERRNO); @@ -2259,6 +2264,7 @@ int btrfs_search_old_slot(struct btrfs_root *root, const struct btrfs_key *key, int level; int lowest_unlock = 1; u8 lowest_level = 0; + struct btrfs_eb_prealloc pa = { 0 }; lowest_level = p->lowest_level; WARN_ON(p->nodes[0] != NULL); @@ -2316,7 +2322,7 @@ int btrfs_search_old_slot(struct btrfs_root *root, const struct btrfs_key *key, goto done; } - ret2 = read_block_for_search(root, p, &b, slot, key); + ret2 = read_block_for_search(root, p, &pa, &b, slot, key); if (ret2 == -EAGAIN && !p->nowait) goto again; if (ret2) { @@ -2339,6 +2345,8 @@ int btrfs_search_old_slot(struct btrfs_root *root, const struct btrfs_key *key, if (ret < 0) btrfs_release_path(p); + btrfs_free_eb_prealloc(&pa); + return ret; } @@ -4780,6 +4788,7 @@ int btrfs_next_old_leaf(struct btrfs_root *root, struct btrfs_path *path, struct extent_buffer *next; struct btrfs_fs_info *fs_info = root->fs_info; struct btrfs_key key; + struct btrfs_eb_prealloc pa = { 0 }; bool need_commit_sem = false; u32 nritems; int ret; @@ -4880,7 +4889,7 @@ int btrfs_next_old_leaf(struct btrfs_root *root, struct btrfs_path *path, } next = c; - ret = read_block_for_search(root, path, &next, slot, &key); + ret = read_block_for_search(root, path, &pa, &next, slot, &key); if (ret == -EAGAIN && !path->nowait) goto again; @@ -4923,7 +4932,7 @@ int btrfs_next_old_leaf(struct btrfs_root *root, struct btrfs_path *path, if (!level) break; - ret = read_block_for_search(root, path, &next, 0, &key); + ret = read_block_for_search(root, path, &pa, &next, 0, &key); if (ret == -EAGAIN && !path->nowait) goto again; @@ -4956,6 +4965,8 @@ int btrfs_next_old_leaf(struct btrfs_root *root, struct btrfs_path *path, ret = ret2; } + btrfs_free_eb_prealloc(&pa); + return ret; } diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c index cc4dcd10631a..819727460bcf 100644 --- a/fs/btrfs/disk-io.c +++ b/fs/btrfs/disk-io.c @@ -591,12 +591,13 @@ static const struct address_space_operations btree_aops = { struct extent_buffer *btrfs_find_create_tree_block( struct btrfs_fs_info *fs_info, + struct btrfs_eb_prealloc *pa, u64 bytenr, u64 owner_root, int level) { if (btrfs_is_testing(fs_info)) return alloc_test_extent_buffer(fs_info, bytenr); - return alloc_extent_buffer(fs_info, bytenr, owner_root, level); + return alloc_extent_buffer(fs_info, pa, bytenr, owner_root, level); } /* @@ -609,12 +610,13 @@ struct extent_buffer *btrfs_find_create_tree_block( struct extent_buffer *read_tree_block(struct btrfs_fs_info *fs_info, u64 bytenr, struct btrfs_tree_parent_check *check) { + struct btrfs_eb_prealloc pa = { 0 }; struct extent_buffer *buf = NULL; int ret; ASSERT(check); - buf = btrfs_find_create_tree_block(fs_info, bytenr, check->owner_root, + buf = btrfs_find_create_tree_block(fs_info, &pa, bytenr, check->owner_root, check->level); if (IS_ERR(buf)) return buf; diff --git a/fs/btrfs/disk-io.h b/fs/btrfs/disk-io.h index 9185f8f02eeb..290508894f7c 100644 --- a/fs/btrfs/disk-io.h +++ b/fs/btrfs/disk-io.h @@ -15,6 +15,7 @@ struct block_device; struct super_block; struct extent_buffer; +struct btrfs_eb_prealloc; struct btrfs_device; struct btrfs_fs_devices; struct btrfs_fs_info; @@ -48,6 +49,7 @@ struct extent_buffer *read_tree_block(struct btrfs_fs_info *fs_info, u64 bytenr, struct btrfs_tree_parent_check *check); struct extent_buffer *btrfs_find_create_tree_block( struct btrfs_fs_info *fs_info, + struct btrfs_eb_prealloc *pa, u64 bytenr, u64 owner_root, int level); int btrfs_start_pre_rw_mount(struct btrfs_fs_info *fs_info); diff --git a/fs/btrfs/extent-tree.c b/fs/btrfs/extent-tree.c index 365735c54e56..d6a4390ee34a 100644 --- a/fs/btrfs/extent-tree.c +++ b/fs/btrfs/extent-tree.c @@ -5260,10 +5260,11 @@ btrfs_init_new_buffer(struct btrfs_trans_handle *trans, struct btrfs_root *root, enum btrfs_lock_nesting nest) { struct btrfs_fs_info *fs_info = root->fs_info; + struct btrfs_eb_prealloc pa = { 0 }; struct extent_buffer *buf; u64 lockdep_owner = owner; - buf = btrfs_find_create_tree_block(fs_info, bytenr, owner, level); + buf = btrfs_find_create_tree_block(fs_info, &pa, bytenr, owner, level); if (IS_ERR(buf)) return buf; @@ -5917,6 +5918,7 @@ static noinline int do_walk_down(struct btrfs_trans_handle *trans, struct walk_control *wc) { struct btrfs_fs_info *fs_info = root->fs_info; + struct btrfs_eb_prealloc pa = { 0 }; u64 bytenr; u64 generation; u64 owner_root = 0; @@ -5939,7 +5941,7 @@ static noinline int do_walk_down(struct btrfs_trans_handle *trans, bytenr = btrfs_node_blockptr(path->nodes[level], path->slots[level]); - next = btrfs_find_create_tree_block(fs_info, bytenr, btrfs_root_id(root), + next = btrfs_find_create_tree_block(fs_info, &pa, bytenr, btrfs_root_id(root), level - 1); if (IS_ERR(next)) return PTR_ERR(next); diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c index 780939756f24..6b332002bad4 100644 --- a/fs/btrfs/extent_io.c +++ b/fs/btrfs/extent_io.c @@ -3629,7 +3629,7 @@ static bool check_eb_alignment(struct btrfs_fs_info *fs_info, u64 start) * The caller needs to free the existing folios and retry using the same order. */ static int attach_eb_folio_to_filemap(struct extent_buffer *eb, int i, - struct btrfs_folio_state *prealloc, + struct btrfs_eb_prealloc *pa, struct extent_buffer **found_eb_ret) { @@ -3651,6 +3651,7 @@ static int attach_eb_folio_to_filemap(struct extent_buffer *eb, int i, if (!ret) goto finish; + /* ret == -EEXIST: a folio already lives at this index. */ existing_folio = filemap_lock_folio(mapping, index + i); /* The page cache only exists for a very short time, just retry. */ if (IS_ERR(existing_folio)) @@ -3659,7 +3660,27 @@ static int attach_eb_folio_to_filemap(struct extent_buffer *eb, int i, /* For now, we should only have single-page folios for btree inode. */ ASSERT(folio_nr_pages(existing_folio) == 1); + /* + * TODO: Special handling for a corner case where the order of + * folios mismatch between the new eb and filemap. + * + * This happens when: + * + * - the new eb is using higher order folio + * + * - the filemap is still using 0-order folios for the range + * This can happen at the previous eb allocation, and we don't + * have higher order folio for the call. + * + * - the existing eb has already been freed + * + * In this case, we have to free the existing folios first, and + * re-allocate using the same order. + * Thankfully this is not going to happen yet, as we're still + * using 0-order folios. + */ if (folio_size(existing_folio) != eb->folio_size) { + DEBUG_WARN("folio order mismatch between new eb and filemap"); folio_unlock(existing_folio); folio_put(existing_folio); return -EAGAIN; @@ -3690,8 +3711,10 @@ static int attach_eb_folio_to_filemap(struct extent_buffer *eb, int i, eb->folio_size = folio_size(eb->folios[i]); eb->folio_shift = folio_shift(eb->folios[i]); /* Should not fail, as we have preallocated the memory. */ - ret = attach_extent_buffer_folio(eb, eb->folios[i], prealloc); + ret = attach_extent_buffer_folio(eb, eb->folios[i], pa->bfs); ASSERT(!ret); + /* The subpage state, if any, is now attached to the folio or freed. */ + pa->bfs = NULL; /* * To inform we have an extra eb under allocation, so that * detach_extent_buffer_page() won't release the folio private when the @@ -3706,13 +3729,89 @@ static int attach_eb_folio_to_filemap(struct extent_buffer *eb, int i, return 0; } +/* + * Allocate the extent_buffer, its folios, and btrfs_folio_state, if needed. + * + * Return 0 on success and a negative errno otherwise. On failure, pa->eb/bfs + * will be NULL. + */ +int btrfs_init_eb_prealloc(struct btrfs_fs_info *fs_info, + struct btrfs_eb_prealloc *pa) +{ + int ret; + + ASSERT(!pa->eb, "unexpected non-null eb: %p", pa->eb); + ASSERT(!pa->bfs, "unexpected non-null bfs: %p", pa->bfs); + + pa->eb = kmem_cache_zalloc(extent_buffer_cache, GFP_NOFS | __GFP_NOFAIL); + /* alloc_eb_folio_array() needs len; init_extent_buffer() sets it again later. */ + pa->eb->len = fs_info->nodesize; + + /* + * Preallocate folio private for subpage case, so that we won't + * allocate memory with i_private_lock nor page lock hold. + * + * The memory will be freed by attach_extent_buffer_page() or freed + * manually if we exit earlier. + */ + if (btrfs_meta_is_subpage(fs_info)) { + pa->bfs = btrfs_alloc_folio_state(fs_info, PAGE_SIZE, + BTRFS_SUBPAGE_METADATA); + if (IS_ERR(pa->bfs)) { + ret = PTR_ERR(pa->bfs); + pa->bfs = NULL; + goto free_eb; + } + } + + /* + * Allocate pages without attaching them. Caller is ultimately responsible + * for attaching the folios to the mapping with attach_eb_folio_to_filemap(). + */ + ret = alloc_eb_folio_array(pa->eb, GFP_NOFS | __GFP_NOFAIL | __GFP_MOVABLE); + if (ret < 0) + goto free_bfs; + + return 0; + +free_bfs: + btrfs_free_folio_state(pa->bfs); + pa->bfs = NULL; +free_eb: + kmem_cache_free(extent_buffer_cache, pa->eb); + pa->eb = NULL; + return ret; +} + +/* + * Used to cleanup a btrfs_eb_prealloc which had its contents allocated but + * folios not yet attached and eb/bfs consumed, and refs still 0. + * + * Safe to call on a fully used btrfs_eb_prealloc as the internal structs will + * be null once they are owned by the context using them. + */ +void btrfs_free_eb_prealloc(struct btrfs_eb_prealloc *pa) +{ + if (!pa->eb) + return; + + for (int i = 0; i < num_extent_pages(pa->eb); i++) { + if (pa->eb->folios[i]) + folio_put(pa->eb->folios[i]); + } + btrfs_free_folio_state(pa->bfs); + kmem_cache_free(extent_buffer_cache, pa->eb); + pa->eb = NULL; + pa->bfs = NULL; +} + struct extent_buffer *alloc_extent_buffer(struct btrfs_fs_info *fs_info, + struct btrfs_eb_prealloc *pa, u64 start, u64 owner_root, int level) { int attached = 0; struct extent_buffer *eb; struct extent_buffer *existing_eb = NULL; - struct btrfs_folio_state *prealloc = NULL; u64 lockdep_owner = owner_root; bool page_contig = true; bool uptodate = true; @@ -3736,7 +3835,13 @@ struct extent_buffer *alloc_extent_buffer(struct btrfs_fs_info *fs_info, if (eb) return eb; - eb = kmem_cache_zalloc(extent_buffer_cache, GFP_NOFS | __GFP_NOFAIL); + if (!pa->eb) { + ret = btrfs_init_eb_prealloc(fs_info, pa); + if (ret) + return ERR_PTR(ret); + } + eb = pa->eb; + pa->eb = NULL; init_extent_buffer(fs_info, eb, start); /* @@ -3748,66 +3853,18 @@ struct extent_buffer *alloc_extent_buffer(struct btrfs_fs_info *fs_info, btrfs_set_buffer_lockdep_class(lockdep_owner, eb, level); - /* - * Preallocate folio private for subpage case, so that we won't - * allocate memory with i_private_lock nor page lock hold. - * - * The memory will be freed by attach_extent_buffer_page() or freed - * manually if we exit earlier. - */ - if (btrfs_meta_is_subpage(fs_info)) { - prealloc = btrfs_alloc_folio_state(fs_info, PAGE_SIZE, BTRFS_SUBPAGE_METADATA); - if (IS_ERR(prealloc)) { - ret = PTR_ERR(prealloc); - goto out; - } - } - -reallocate: - /* - * Allocate all pages first. These will be attached to btree_inode->i_mapping - * below (added to LRU, served by btree_migrate_folio), so request - * __GFP_MOVABLE so the page allocator places them in MOVABLE pageblocks. - */ - ret = alloc_eb_folio_array(eb, GFP_NOFS | __GFP_NOFAIL | __GFP_MOVABLE); - if (ret < 0) { - btrfs_free_folio_state(prealloc); - goto out; - } - /* Attach all pages to the filemap. */ for (int i = 0; i < num_extent_folios(eb); i++) { struct folio *folio; - ret = attach_eb_folio_to_filemap(eb, i, prealloc, &existing_eb); + ret = attach_eb_folio_to_filemap(eb, i, pa, &existing_eb); if (ret > 0) { ASSERT(existing_eb); goto out; } - - /* - * TODO: Special handling for a corner case where the order of - * folios mismatch between the new eb and filemap. - * - * This happens when: - * - * - the new eb is using higher order folio - * - * - the filemap is still using 0-order folios for the range - * This can happen at the previous eb allocation, and we don't - * have higher order folio for the call. - * - * - the existing eb has already been freed - * - * In this case, we have to free the existing folios first, and - * re-allocate using the same order. - * Thankfully this is not going to happen yet, as we're still - * using 0-order folios. - */ - if (unlikely(ret == -EAGAIN)) { - DEBUG_WARN("folio order mismatch between new eb and filemap"); - goto reallocate; - } + /* -EAGAIN: folio order mismatch, unreachable with 0-order folios. */ + if (ret < 0) + goto out; attached++; /* @@ -3884,6 +3941,10 @@ struct extent_buffer *alloc_extent_buffer(struct btrfs_fs_info *fs_info, out: WARN_ON(!refcount_dec_and_test(&eb->refs)); + /* Attach hands off pa->bfs; free it if we bailed first. */ + btrfs_free_folio_state(pa->bfs); + pa->bfs = NULL; + /* * Any attached folios need to be detached before we unlock them. This * is because when we're inserting our new folios into the mapping, and @@ -4980,6 +5041,7 @@ void btrfs_readahead_tree_block(struct btrfs_fs_info *fs_info, .level = level, .transid = gen }; + struct btrfs_eb_prealloc pa = { 0 }; struct extent_buffer *eb; int ret; @@ -4988,7 +5050,7 @@ void btrfs_readahead_tree_block(struct btrfs_fs_info *fs_info, check.has_first_key = true; } - eb = btrfs_find_create_tree_block(fs_info, bytenr, owner_root, level); + eb = btrfs_find_create_tree_block(fs_info, &pa, bytenr, owner_root, level); if (IS_ERR(eb)) return; diff --git a/fs/btrfs/extent_io.h b/fs/btrfs/extent_io.h index ad4ffce32702..bfa61d9ee4af 100644 --- a/fs/btrfs/extent_io.h +++ b/fs/btrfs/extent_io.h @@ -119,6 +119,21 @@ struct extent_buffer { #endif }; +/* + * Wrapper struct for managing preallocating an extent_buffer, its folios and a + * btrfs_folio_state if needed. + * + * Only used to mediate allocation, do not refer to the eb directly if not + * returned from a successful eb allocating API. + * + * The eb folios and bfs should generally not be fully attached, except briefly + * before they are NULLed in the struct after successful attachment. + */ +struct btrfs_eb_prealloc { + struct extent_buffer *eb; + struct btrfs_folio_state *bfs; +}; + struct btrfs_eb_write_context { struct writeback_control *wbc; struct extent_buffer *eb; @@ -271,7 +286,11 @@ int set_folio_extent_mapped(struct folio *folio); void clear_folio_extent_mapped(struct folio *folio); struct extent_buffer *alloc_extent_buffer(struct btrfs_fs_info *fs_info, + struct btrfs_eb_prealloc *pa, u64 start, u64 owner_root, int level); +int btrfs_init_eb_prealloc(struct btrfs_fs_info *fs_info, + struct btrfs_eb_prealloc *pa); +void btrfs_free_eb_prealloc(struct btrfs_eb_prealloc *pa); struct extent_buffer *alloc_dummy_extent_buffer(struct btrfs_fs_info *fs_info, u64 start); struct extent_buffer *btrfs_clone_extent_buffer(const struct extent_buffer *src); diff --git a/fs/btrfs/tree-log.c b/fs/btrfs/tree-log.c index 47046dd14997..f6573dac4dce 100644 --- a/fs/btrfs/tree-log.c +++ b/fs/btrfs/tree-log.c @@ -2969,6 +2969,7 @@ static noinline int walk_down_log_tree(struct btrfs_path *path, int *level, { struct btrfs_trans_handle *trans = wc->trans; struct btrfs_fs_info *fs_info = wc->log->fs_info; + struct btrfs_eb_prealloc pa = { 0 }; u64 bytenr; u64 ptr_gen; struct extent_buffer *next; @@ -2993,7 +2994,7 @@ static noinline int walk_down_log_tree(struct btrfs_path *path, int *level, check.has_first_key = true; btrfs_node_key_to_cpu(cur, &check.first_key, path->slots[*level]); - next = btrfs_find_create_tree_block(fs_info, bytenr, + next = btrfs_find_create_tree_block(fs_info, &pa, bytenr, btrfs_header_owner(cur), *level - 1); if (IS_ERR(next)) { From 6b338068ac373d1f14e693132cb17f984b8aced7 Mon Sep 17 00:00:00 2001 From: Boris Burkov Date: Tue, 21 Jul 2026 15:42:14 -0700 Subject: [PATCH 69/72] btrfs: enable unlocked NOFAIL retry for eb allocations Now that we have the btrfs_eb_prealloc struct to carry the allocation and the "needs prealloc" signal, wire that up between the various search_slot style callers down into alloc_extent_buffer. If the prealloc struct indicates that it supports a nowait try, then alloc_extent_buffer tries to allocate NOWAIT. If that succeeds, great. Otherwise, we return EAGAIN and signal via the struct that preallocation is required. The caller then does the allocation and tries again with the eb, bfs, and folios wired through in the prealloc struct. If unlock-and-allocate retries are not supported then we just use the normal gfp flags like before. Note that there are still two GFP_NOFS allocations, as far as I know, that happen under the lock and cannot be preallocated: - the __xa_cmpxchg to insert the eb into the eb xarray - the xarray allocations for filemap_add_folio to add the folios to the btree_inode mapping. The former we could wire up with xa_reserve if we signaled the "prealloc start" back up to the retry point. However, since there is no concept of reservation in the filemap xarray, it seemed relatively unhelpful to bother. These allocations are relatively small cached slab allocations, so hopefully we can move the needle on reclaim stalls without reserving them. Reviewed-by: Jeff Layton Reviewed-by: Filipe Manana Signed-off-by: Boris Burkov Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/ctree.c | 21 ++++++++++++++++++--- fs/btrfs/extent_io.c | 27 +++++++++++++++++++++------ fs/btrfs/extent_io.h | 6 +++++- fs/btrfs/subpage.c | 7 ++++--- fs/btrfs/subpage.h | 3 ++- 5 files changed, 50 insertions(+), 14 deletions(-) diff --git a/fs/btrfs/ctree.c b/fs/btrfs/ctree.c index 261ef4ec7d1b..8fe330d81b8f 100644 --- a/fs/btrfs/ctree.c +++ b/fs/btrfs/ctree.c @@ -2006,7 +2006,7 @@ int btrfs_search_slot(struct btrfs_trans_handle *trans, struct btrfs_root *root, u8 lowest_level = 0; int min_write_lock_level; int prev_cmp; - struct btrfs_eb_prealloc pa = { 0 }; + struct btrfs_eb_prealloc pa = { .supports_nowait = true }; if (!root) return -EINVAL; @@ -2061,6 +2061,11 @@ int btrfs_search_slot(struct btrfs_trans_handle *trans, struct btrfs_root *root, } again: + if (pa.needs_prealloc) { + ret = btrfs_init_eb_prealloc(fs_info, &pa, false); + if (ret) + goto done; + } prev_cmp = -1; b = btrfs_search_slot_get_root(root, p, write_lock_level); if (IS_ERR(b)) { @@ -2264,7 +2269,7 @@ int btrfs_search_old_slot(struct btrfs_root *root, const struct btrfs_key *key, int level; int lowest_unlock = 1; u8 lowest_level = 0; - struct btrfs_eb_prealloc pa = { 0 }; + struct btrfs_eb_prealloc pa = { .supports_nowait = true }; lowest_level = p->lowest_level; WARN_ON(p->nodes[0] != NULL); @@ -2276,6 +2281,11 @@ int btrfs_search_old_slot(struct btrfs_root *root, const struct btrfs_key *key, } again: + if (pa.needs_prealloc) { + ret = btrfs_init_eb_prealloc(fs_info, &pa, false); + if (ret) + goto done; + } b = btrfs_get_old_root(root, time_seq); if (unlikely(!b)) { ret = -EIO; @@ -4788,7 +4798,7 @@ int btrfs_next_old_leaf(struct btrfs_root *root, struct btrfs_path *path, struct extent_buffer *next; struct btrfs_fs_info *fs_info = root->fs_info; struct btrfs_key key; - struct btrfs_eb_prealloc pa = { 0 }; + struct btrfs_eb_prealloc pa = { .supports_nowait = true }; bool need_commit_sem = false; u32 nritems; int ret; @@ -4807,6 +4817,11 @@ int btrfs_next_old_leaf(struct btrfs_root *root, struct btrfs_path *path, btrfs_item_key_to_cpu(path->nodes[0], &key, nritems - 1); again: + if (pa.needs_prealloc) { + ret = btrfs_init_eb_prealloc(fs_info, &pa, false); + if (ret) + goto done; + } level = 1; next = NULL; btrfs_release_path(path); diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c index 6b332002bad4..ee8b062a3b64 100644 --- a/fs/btrfs/extent_io.c +++ b/fs/btrfs/extent_io.c @@ -3732,18 +3732,28 @@ static int attach_eb_folio_to_filemap(struct extent_buffer *eb, int i, /* * Allocate the extent_buffer, its folios, and btrfs_folio_state, if needed. * + * @pa: The holder struct to do the allocation in. + * @nowait: Whether to do a speculative GFP_NOWAIT allocation while holding locks. + * * Return 0 on success and a negative errno otherwise. On failure, pa->eb/bfs - * will be NULL. + * will be NULL. If @nowait=true, then on ENOMEM, mark @pa->needs_prealloc and + * return -EAGAIN to signal the caller to unlock and retry. */ int btrfs_init_eb_prealloc(struct btrfs_fs_info *fs_info, - struct btrfs_eb_prealloc *pa) + struct btrfs_eb_prealloc *pa, bool nowait) { + gfp_t gfp = nowait ? GFP_NOWAIT : GFP_NOFS | __GFP_NOFAIL; int ret; ASSERT(!pa->eb, "unexpected non-null eb: %p", pa->eb); ASSERT(!pa->bfs, "unexpected non-null bfs: %p", pa->bfs); + pa->needs_prealloc = false; - pa->eb = kmem_cache_zalloc(extent_buffer_cache, GFP_NOFS | __GFP_NOFAIL); + pa->eb = kmem_cache_zalloc(extent_buffer_cache, gfp); + if (!pa->eb) { + ret = -ENOMEM; + goto out; + } /* alloc_eb_folio_array() needs len; init_extent_buffer() sets it again later. */ pa->eb->len = fs_info->nodesize; @@ -3756,7 +3766,7 @@ int btrfs_init_eb_prealloc(struct btrfs_fs_info *fs_info, */ if (btrfs_meta_is_subpage(fs_info)) { pa->bfs = btrfs_alloc_folio_state(fs_info, PAGE_SIZE, - BTRFS_SUBPAGE_METADATA); + BTRFS_SUBPAGE_METADATA, gfp); if (IS_ERR(pa->bfs)) { ret = PTR_ERR(pa->bfs); pa->bfs = NULL; @@ -3768,7 +3778,7 @@ int btrfs_init_eb_prealloc(struct btrfs_fs_info *fs_info, * Allocate pages without attaching them. Caller is ultimately responsible * for attaching the folios to the mapping with attach_eb_folio_to_filemap(). */ - ret = alloc_eb_folio_array(pa->eb, GFP_NOFS | __GFP_NOFAIL | __GFP_MOVABLE); + ret = alloc_eb_folio_array(pa->eb, gfp | __GFP_MOVABLE); if (ret < 0) goto free_bfs; @@ -3780,6 +3790,11 @@ int btrfs_init_eb_prealloc(struct btrfs_fs_info *fs_info, free_eb: kmem_cache_free(extent_buffer_cache, pa->eb); pa->eb = NULL; +out: + if (nowait && ret == -ENOMEM) { + pa->needs_prealloc = true; + ret = -EAGAIN; + } return ret; } @@ -3836,7 +3851,7 @@ struct extent_buffer *alloc_extent_buffer(struct btrfs_fs_info *fs_info, return eb; if (!pa->eb) { - ret = btrfs_init_eb_prealloc(fs_info, pa); + ret = btrfs_init_eb_prealloc(fs_info, pa, pa->supports_nowait); if (ret) return ERR_PTR(ret); } diff --git a/fs/btrfs/extent_io.h b/fs/btrfs/extent_io.h index bfa61d9ee4af..d8dd2ae9ff9a 100644 --- a/fs/btrfs/extent_io.h +++ b/fs/btrfs/extent_io.h @@ -132,6 +132,10 @@ struct extent_buffer { struct btrfs_eb_prealloc { struct extent_buffer *eb; struct btrfs_folio_state *bfs; + /* eb alloc may use GFP_NOWAIT; caller can drop locks and retry. */ + bool supports_nowait; + /* GFP_NOWAIT eb alloc failed; preallocate again and retry. */ + bool needs_prealloc; }; struct btrfs_eb_write_context { @@ -289,7 +293,7 @@ struct extent_buffer *alloc_extent_buffer(struct btrfs_fs_info *fs_info, struct btrfs_eb_prealloc *pa, u64 start, u64 owner_root, int level); int btrfs_init_eb_prealloc(struct btrfs_fs_info *fs_info, - struct btrfs_eb_prealloc *pa); + struct btrfs_eb_prealloc *pa, bool nowait); void btrfs_free_eb_prealloc(struct btrfs_eb_prealloc *pa); struct extent_buffer *alloc_dummy_extent_buffer(struct btrfs_fs_info *fs_info, u64 start); diff --git a/fs/btrfs/subpage.c b/fs/btrfs/subpage.c index 27dd677ca687..ebf18efe1ea3 100644 --- a/fs/btrfs/subpage.c +++ b/fs/btrfs/subpage.c @@ -59,7 +59,7 @@ int btrfs_attach_folio_state(const struct btrfs_fs_info *fs_info, if (type == BTRFS_SUBPAGE_DATA && !btrfs_is_subpage(fs_info, folio)) return 0; - bfs = btrfs_alloc_folio_state(fs_info, folio_size(folio), type); + bfs = btrfs_alloc_folio_state(fs_info, folio_size(folio), type, GFP_NOFS); if (IS_ERR(bfs)) return PTR_ERR(bfs); @@ -86,7 +86,8 @@ void btrfs_detach_folio_state(const struct btrfs_fs_info *fs_info, struct folio } struct btrfs_folio_state *btrfs_alloc_folio_state(const struct btrfs_fs_info *fs_info, - size_t fsize, enum btrfs_folio_type type) + size_t fsize, enum btrfs_folio_type type, + gfp_t gfp) { struct btrfs_folio_state *ret; unsigned int real_size; @@ -96,7 +97,7 @@ struct btrfs_folio_state *btrfs_alloc_folio_state(const struct btrfs_fs_info *fs real_size = struct_size(ret, bitmaps, BITS_TO_LONGS(btrfs_bitmap_nr_max * (fsize >> fs_info->sectorsize_bits))); - ret = kzalloc(real_size, GFP_NOFS); + ret = kzalloc(real_size, gfp); if (!ret) return ERR_PTR(-ENOMEM); diff --git a/fs/btrfs/subpage.h b/fs/btrfs/subpage.h index 9aceba93c818..9b106a73d682 100644 --- a/fs/btrfs/subpage.h +++ b/fs/btrfs/subpage.h @@ -110,7 +110,8 @@ void btrfs_detach_folio_state(const struct btrfs_fs_info *fs_info, struct folio /* Allocate additional data where page represents more than one sector */ struct btrfs_folio_state *btrfs_alloc_folio_state(const struct btrfs_fs_info *fs_info, - size_t fsize, enum btrfs_folio_type type); + size_t fsize, enum btrfs_folio_type type, + gfp_t gfp); static inline void btrfs_free_folio_state(struct btrfs_folio_state *bfs) { kfree(bfs); From 536e94a4c347d26aaac0bb1a2ef879d0743ac234 Mon Sep 17 00:00:00 2001 From: Boris Burkov Date: Tue, 21 Jul 2026 15:42:15 -0700 Subject: [PATCH 70/72] btrfs: use GFP_NOWAIT for tree block readahead extent_buffer readahead should not be able to painfully stall a search_slot and hog tree locks by getting stuck in direct reclaim. If the allocation fails, that is fine, we simply fail to do the readahead in that case. Reviewed-by: Jeff Layton Reviewed-by: Filipe Manana Signed-off-by: Boris Burkov Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/extent_io.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c index ee8b062a3b64..fa9f45cd7652 100644 --- a/fs/btrfs/extent_io.c +++ b/fs/btrfs/extent_io.c @@ -5056,7 +5056,8 @@ void btrfs_readahead_tree_block(struct btrfs_fs_info *fs_info, .level = level, .transid = gen }; - struct btrfs_eb_prealloc pa = { 0 }; + /* Readahead is best effort so prefer to fail rather than block in reclaim. */ + struct btrfs_eb_prealloc pa = { .supports_nowait = true }; struct extent_buffer *eb; int ret; From 6003c20dbd87ec5ab68ef9c1872096b4915ab195 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Fri, 31 Jul 2026 10:14:50 +0930 Subject: [PATCH 71/72] btrfs: add extra ASSERT()s to make sure the folio size is correct Inspired by the previous crash exposed by generic/795, we want to make sure every folio from btrfs page cache is properly aligned to block size. This is especially important for bs > ps support, as every btrfs infrastructure, e.g. extent map and extent state, requires strong block alignment checks. Furthermore, also output the minimal folio order from the inode mapping, which is the determining factor during debugging, helping a lot pinning down the final cause. Reviewed-by: Filipe Manana Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/extent_io.c | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/fs/btrfs/extent_io.c b/fs/btrfs/extent_io.c index fa9f45cd7652..d7600e5fa3d9 100644 --- a/fs/btrfs/extent_io.c +++ b/fs/btrfs/extent_io.c @@ -1392,6 +1392,22 @@ static void lock_extents_for_read(struct btrfs_inode *inode, u64 start, u64 end, } } +static void assert_folio_range(const struct btrfs_inode *inode, + u64 start, u64 end) +{ + const u32 blocksize = inode->root->fs_info->sectorsize; + + /* + * For btrfs page cache, a folio always contains at least one block, + * so the range should always be block size aligned. + */ + ASSERT(IS_ALIGNED(start, blocksize) && IS_ALIGNED(end + 1, blocksize), + "blocksize=%u root=%lld ino=%llu start=%llu end=%llu mapping min order=%u", + blocksize, btrfs_root_id(inode->root), btrfs_ino(inode), + start, end, + mapping_min_folio_order(inode->vfs_inode.i_mapping)); +} + int btrfs_read_folio(struct file *file, struct folio *folio) { struct inode *vfs_inode = folio->mapping->host; @@ -1407,6 +1423,7 @@ int btrfs_read_folio(struct file *file, struct folio *folio) struct fsverity_info *vi = NULL; int ret; + assert_folio_range(inode, start, end); lock_extents_for_read(inode, start, end, &cached_state); if (folio_pos(folio) < i_size_read(vfs_inode)) vi = fsverity_get_info(vfs_inode); @@ -1914,6 +1931,7 @@ static noinline_for_stack int extent_writepage_io(struct btrfs_inode *inode, ASSERT(start >= folio_start, "start=%llu folio_start=%llu", start, folio_start); ASSERT(end <= folio_end, "start=%llu len=%u folio_start=%llu folio_size=%zu", start, len, folio_start, folio_size(folio)); + assert_folio_range(inode, folio_start, folio_end - 1); /* * We are about to checksum and write out the data, so it must not be @@ -2976,6 +2994,7 @@ void btrfs_readahead(struct readahead_control *rac) struct extent_map *em_cached = NULL; struct fsverity_info *vi = NULL; + assert_folio_range(inode, start, end); lock_extents_for_read(inode, start, end, &cached_state); /* We don't use cached state for a bulk unlock, just free it. */ btrfs_free_extent_state(cached_state); From 4096e2a06f43fae209d6472d395f3219e02ab2e6 Mon Sep 17 00:00:00 2001 From: Filipe Manana Date: Thu, 30 Jul 2026 16:31:35 +0100 Subject: [PATCH 72/72] btrfs: skip hole detection during full fsync for files without holes If we the no-holes feature is enabled (a default since btrfs-progs 5.15), when doing a full fsync we always iterate of all leaves in the subvolume root that contain file extent items in order to detect holes between them. This can take a lot of time for files with a large number of extents. But if we know there are no prealloc extents and the amount of space (uncompressed space) is greater than or equals to the i_size of the inode, then we cannot have holes and therefore avoid searching for them. So skip the search if those conditions are met. The following test script was used: $ cat test.sh #!/bin/bash MNT=/mnt/nullb0 DEV=/dev/nullb0 umount $MNT &> /dev/null mkfs.btrfs -f $DEV mount $DEV $MNT # 256M gives 64K extents of 4K each. FILE_SIZE=$((256 * 1024 * 1024)) touch $MNT/foobar for ((i = 0; i < $FILE_SIZE; i += 8192)); do xfs_io -c "pwrite -S 0xab $i 4K" $MNT/foobar > /dev/null done xfs_io -c "fsync" $MNT/foobar for ((i = 4096; i < $FILE_SIZE; i += 8192)); do xfs_io -c "pwrite -S 0xab $i 4K" $MNT/foobar > /dev/null done # unmount and mount, clear caches and ensure the next fsync is a # full sync. umount $MNT mount $DEV $MNT # Do some change to the file in order to fsync. xfs_io -c "pwrite -S 0xcd 0 4K" $MNT/foobar > /dev/null T0=$(date +%s%N) xfs_io -c "fsync" $MNT/foobar T1=$(date +%s%N) echo echo "Took $(( (T1 - T0) / 1000 ))us" umount $MNT Before this change: Took 28721us After this change: Took 5453us That's about 5.3x times faster. Reviewed-by: Qu Wenruo Signed-off-by: Filipe Manana Signed-off-by: David Sterba --- fs/btrfs/tree-log.c | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/fs/btrfs/tree-log.c b/fs/btrfs/tree-log.c index f6573dac4dce..7ba7b6098aa5 100644 --- a/fs/btrfs/tree-log.c +++ b/fs/btrfs/tree-log.c @@ -5592,6 +5592,15 @@ static int btrfs_log_holes(struct btrfs_trans_handle *trans, if (!btrfs_fs_incompat(fs_info, NO_HOLES) || i_size == 0) return 0; + /* + * If there are no prealloc extents (which can be located past i_size), + * and disk space used is greater than or equals to i_size, then there + * are no holes. + */ + if (!(inode->flags & BTRFS_INODE_PREALLOC) && + i_size <= inode_get_bytes(&inode->vfs_inode)) + return 0; + key.objectid = ino; key.type = BTRFS_EXTENT_DATA_KEY; key.offset = 0;