From 6881f45d0eb541f2cee8c37c84b3860a23823bb3 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Sat, 4 Jul 2026 17:58:56 +0930 Subject: [PATCH 01/10] btrfs: fix leaking BTRFS_FS_STATE_REMOUNTING flag [BUG] The following script can lead to unexpected qgroup rescan failure: # mkfs.btrfs -f -O quota $dev # mount $dev $mnt # mount -o remount,rescue=ibadroots $mnt ^^^^^ This above command is expected to fail # btrfs quota rescan -w $mnt ^^^^^ The above qgroup rescan is not expected to fail # btrfs qgroup show $mnt WARNING: qgroup data inconsistent, rescan recommended Qgroupid Referenced Exclusive Path -------- ---------- --------- ---- 0/5 16.00KiB 16.00KiB The above short script will be converted to a proper fstests case. [CAUSE] Inside btrfs_reconfigure(), if either btrfs_check_options() or btrfs_check_features() failed, we will always have BTRFS_FS_STATE_REMOUNTING set for the fs until the next successful remount. That BTRFS_FS_STATE_REMOUNTING flag will interrupt several operations, including: - Qgroup rescan - Auto defrag - Space reclaim [FIX] Change the error handling of btrfs_check_options() and btrfs_check_features() to goto restore label. Fixes: eddb1a433f26 ("btrfs: add reconfigure callback for fs_context") Reviewed-by: Johannes Thumshirn Signed-off-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/super.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/fs/btrfs/super.c b/fs/btrfs/super.c index c946bccf0748..63bf1f1e16d4 100644 --- a/fs/btrfs/super.c +++ b/fs/btrfs/super.c @@ -1519,12 +1519,14 @@ static int btrfs_reconfigure(struct fs_context *fc) sync_filesystem(sb); set_bit(BTRFS_FS_STATE_REMOUNTING, &fs_info->fs_state); - if (!btrfs_check_options(fs_info, &ctx->mount_opt, fc->sb_flags)) - return -EINVAL; + if (!btrfs_check_options(fs_info, &ctx->mount_opt, fc->sb_flags)) { + ret = -EINVAL; + goto restore; + } ret = btrfs_check_features(fs_info, !(fc->sb_flags & SB_RDONLY)); if (ret < 0) - return ret; + goto restore; btrfs_ctx_to_info(fs_info, ctx); btrfs_remount_begin(fs_info, old_ctx.mount_opt, fc->sb_flags); From 1ebe51c29fa9755d5b2fea28727c051117907cf8 Mon Sep 17 00:00:00 2001 From: Johannes Thumshirn Date: Fri, 3 Jul 2026 07:54:40 +0200 Subject: [PATCH 02/10] btrfs: zoned: fix deadlock between metadata writeback and transaction commit When writing out metadata extent buffers in a zoned filesystem, btree_writepages() holds fs_info->zoned_meta_io_lock across the whole writeback loop, including the call to btrfs_check_meta_write_pointer() -> check_bg_is_active(). For the tree-log block group, check_bg_is_active() may fail to activate the zone and fall back to btrfs_zone_finish_one_bg() to free an active zone. That path waits for the running transaction to commit while still holding zoned_meta_io_lock, but the committer needs that same lock to write out the tree extents, so the two tasks deadlock: Task A (kworker, metadata writeback) Task B (fsstress, transaction commit) ------------------------------------ ------------------------------------- wb_workfn() btrfs_commit_transaction(T) btree_writepages() btrfs_write_and_wait_transaction() btrfs_zoned_meta_io_lock() btrfs_write_marked_extents() btrfs_check_meta_write_pointer() btree_writepages() check_bg_is_active() [treelog_bg] btrfs_zoned_meta_io_lock() btrfs_zone_finish_one_bg() do_zone_finish() btrfs_inc_block_group_ro() btrfs_wait_for_commit() The sibling branch in check_bg_is_active() already drops zoned_meta_io_lock around do_zone_finish() for this exact reason. Do the same in the tree-log branch: release the lock around btrfs_zone_finish_one_bg() and re-acquire it afterwards. The lock only protects fs_info->active_{meta,system}_bg, which this branch does not touch, and ctx->zoned_bg keeps a reference to the block group across the unlock, so nothing is lost while the lock is dropped. This hang occasionally reproduces with fstests generic/475 on a zoned btrfs filesystem. Fixes: 13bb483d32ab ("btrfs: zoned: activate metadata block group on write time") Reviewed-by: Naohiro Aota Signed-off-by: Johannes Thumshirn Signed-off-by: David Sterba --- fs/btrfs/zoned.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/fs/btrfs/zoned.c b/fs/btrfs/zoned.c index 97f06dd01693..44a13ed6b8b2 100644 --- a/fs/btrfs/zoned.c +++ b/fs/btrfs/zoned.c @@ -2190,7 +2190,11 @@ static bool check_bg_is_active(struct btrfs_eb_write_context *ctx, if (fs_info->treelog_bg == block_group->start) { if (!btrfs_zone_activate(block_group)) { - int ret_fin = btrfs_zone_finish_one_bg(fs_info); + int ret_fin; + + btrfs_zoned_meta_io_unlock(fs_info); + ret_fin = btrfs_zone_finish_one_bg(fs_info); + btrfs_zoned_meta_io_lock(fs_info); if (ret_fin != 1 || !btrfs_zone_activate(block_group)) return false; From 5fabb1cf25d723274009d7b759545fd59f230c9d Mon Sep 17 00:00:00 2001 From: Johannes Thumshirn Date: Fri, 3 Jul 2026 07:54:45 +0200 Subject: [PATCH 03/10] btrfs: zoned: reset meta_write_pointer on zone reset btrfs_reset_unused_block_groups() resets a block group's zone and sets alloc_offset back to 0 so the space can be reused, but it leaves meta_write_pointer pointing at the previous end of the zone. Once the block group is reactivated and reused for metadata, newly allocated tree blocks live before that stale write pointer. btrfs_check_meta_write_pointer() then sees them behind the write pointer, so they can never be written out in sequential order: the dirty extent buffers are stranded and pin their btree_inode folios until unmount. Reset meta_write_pointer back to the start of the block group for metadata and system block groups. Fixes: 453a73c3069a ("btrfs: zoned: reclaim unused zone by zone resetting") Reviewed-by: Naohiro Aota Signed-off-by: Johannes Thumshirn Signed-off-by: David Sterba --- fs/btrfs/zoned.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/fs/btrfs/zoned.c b/fs/btrfs/zoned.c index 44a13ed6b8b2..0706c0788cb2 100644 --- a/fs/btrfs/zoned.c +++ b/fs/btrfs/zoned.c @@ -3189,6 +3189,17 @@ int btrfs_reset_unused_block_groups(struct btrfs_space_info *space_info, u64 num reclaimed = bg->alloc_offset; bg->zone_unusable = bg->length - bg->zone_capacity; bg->alloc_offset = 0; + /* + * The zone was just reset to empty, so alloc_offset went back to + * the start of the zone. For metadata/system block groups the + * write pointer must follow it back to the start of the zone; + * otherwise it stays stale at the previous (finished) zone end, + * and metadata written into the reused zone would sit behind the + * write pointer, could never be written out in sequential order, + * and would be stranded (pinning its folio) until unmount. + */ + if (bg->flags & (BTRFS_BLOCK_GROUP_METADATA | BTRFS_BLOCK_GROUP_SYSTEM)) + bg->meta_write_pointer = bg->start; /* * This holds because we currently reset fully used then freed * block group. From 51a0e8399858621442807a26057bcd1cd3ced046 Mon Sep 17 00:00:00 2001 From: Dongjiang Zhu Date: Mon, 13 Jul 2026 16:50:08 +0800 Subject: [PATCH 04/10] btrfs: skip global block reserve accounting for rescue mounts [BUG] Mounting with rescue=ibadroots after corrupting the block group tree root triggers a NULL pointer dereference: BUG: kernel NULL pointer dereference, address: 0000000000000100 RIP: 0010:btrfs_update_global_block_rsv+0x9d/0x1c0 [btrfs] Call Trace: fill_dummy_bgs+0xd4/0x120 [btrfs] open_ctree+0xc6e/0x1ca0 [btrfs] btrfs_get_tree+0x50d/0xa40 [btrfs] The same crash occurs with a corrupted raid stripe tree root, via btrfs_read_block_groups() instead of fill_dummy_bgs(). [CAUSE] With rescue=ibadroots, btrfs_read_roots() allows the mount to continue when either root cannot be read, leaving the corresponding root pointer NULL while its on-disk feature bit remains set. btrfs_update_global_block_rsv() then dereferences the missing root based on the feature bit alone. [FIX] Rescue mounts are fully read-only and cannot start transactions, so the global reserve is never consumed. Under btrfs_is_full_ro(), mark the reserve as full and return before performing the accounting. And since we need to check if the fs is mount fully RO, export fs_is_full_ro() as btrfs_is_full_ro(), and move it to fs.h. Fixes: 8dbfc14fc736 ("btrfs: account block group tree when calculating global reserve size") Fixes: 515020900d44 ("btrfs: read raid stripe tree from disk") Suggested-by: Qu Wenruo Signed-off-by: Dongjiang Zhu [ Squash the fs_is_full_ro() export commit into this one. ] Reviewed-by: Qu Wenruo Signed-off-by: David Sterba --- fs/btrfs/block-rsv.c | 19 +++++++++++++++++-- fs/btrfs/disk-io.c | 11 +---------- fs/btrfs/fs.h | 9 +++++++++ 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/fs/btrfs/block-rsv.c b/fs/btrfs/block-rsv.c index 9efb3016ef11..c68a8f4b7d19 100644 --- a/fs/btrfs/block-rsv.c +++ b/fs/btrfs/block-rsv.c @@ -322,10 +322,25 @@ void btrfs_block_rsv_add_bytes(struct btrfs_block_rsv *block_rsv, void btrfs_update_global_block_rsv(struct btrfs_fs_info *fs_info) { struct btrfs_block_rsv *block_rsv = &fs_info->global_block_rsv; - struct btrfs_space_info *sinfo = block_rsv->space_info; + struct btrfs_space_info *sinfo; struct btrfs_root *root, *tmp; - u64 num_bytes = btrfs_root_used(&fs_info->tree_root->root_item); unsigned int min_items = 1; + u64 num_bytes; + + /* + * A full read-only mount (rescue options) cannot start transactions, + * so the global reserve is never consumed. Mark it as full and skip + * the accounting. + */ + if (btrfs_is_full_ro(fs_info)) { + spin_lock(&block_rsv->lock); + block_rsv->full = true; + spin_unlock(&block_rsv->lock); + return; + } + + sinfo = block_rsv->space_info; + num_bytes = btrfs_root_used(&fs_info->tree_root->root_item); /* * The global block rsv is based on the size of the extent tree, the diff --git a/fs/btrfs/disk-io.c b/fs/btrfs/disk-io.c index 0a7d80da9c94..36332df9a0f1 100644 --- a/fs/btrfs/disk-io.c +++ b/fs/btrfs/disk-io.c @@ -3288,15 +3288,6 @@ int btrfs_check_features(struct btrfs_fs_info *fs_info, bool is_rw_mount) return 0; } -static bool fs_is_full_ro(const struct btrfs_fs_info *fs_info) -{ - if (!sb_rdonly(fs_info->sb)) - return false; - if (unlikely(fs_info->mount_opt & BTRFS_MOUNT_FULL_RO_MASK)) - return true; - return false; -} - /* * Try to wait for any metadata readahead, and invalidate all btree folios. * @@ -3462,7 +3453,7 @@ int __cold open_ctree(struct super_block *sb, struct btrfs_fs_devices *fs_device WRITE_ONCE(fs_info->fs_error, -EUCLEAN); /* If the fs has any rescue options, no transaction is allowed. */ - if (fs_is_full_ro(fs_info)) + if (btrfs_is_full_ro(fs_info)) WRITE_ONCE(fs_info->fs_error, -EROFS); /* Set up fs_info before parsing mount options */ diff --git a/fs/btrfs/fs.h b/fs/btrfs/fs.h index 5f0cfb0b5466..7ee9ec2b0efb 100644 --- a/fs/btrfs/fs.h +++ b/fs/btrfs/fs.h @@ -1159,6 +1159,15 @@ void __btrfs_clear_fs_compat_ro(struct btrfs_fs_info *fs_info, u64 flag, #define btrfs_test_opt(fs_info, opt) ((fs_info)->mount_opt & \ BTRFS_MOUNT_##opt) +static inline bool btrfs_is_full_ro(const struct btrfs_fs_info *fs_info) +{ + if (!sb_rdonly(fs_info->sb)) + return false; + if (unlikely(fs_info->mount_opt & BTRFS_MOUNT_FULL_RO_MASK)) + return true; + return false; +} + static inline bool btrfs_fs_closing(const struct btrfs_fs_info *fs_info) { return unlikely(test_bit(BTRFS_FS_CLOSING_START, &fs_info->flags)); From c438d34ec1eed4d23e2081d61c5e96f5176898a9 Mon Sep 17 00:00:00 2001 From: Dongjiang Zhu Date: Mon, 13 Jul 2026 16:50:10 +0800 Subject: [PATCH 05/10] btrfs: report missing raid stripe tree root during lookup When rescue=ibadroots ignores a failure to load the raid stripe tree root, fs_info->stripe_root remains NULL. After the rescue mount proceeds, reading file data that requires the raid stripe tree reaches btrfs_get_raid_extent_offset(). Currently btrfs_search_slot() handles the NULL root and returns -EINVAL. This avoids a NULL pointer dereference, but provides no diagnostic and incorrectly describes missing filesystem metadata as an invalid argument. Check stripe_root before allocating a path, emit a rate-limited error with the logical address, and return -EUCLEAN. Lookups with a valid stripe root are unchanged. Reviewed-by: Qu Wenruo Signed-off-by: Dongjiang Zhu Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/raid-stripe-tree.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/fs/btrfs/raid-stripe-tree.c b/fs/btrfs/raid-stripe-tree.c index 454a95bf542a..b210371ce91e 100644 --- a/fs/btrfs/raid-stripe-tree.c +++ b/fs/btrfs/raid-stripe-tree.c @@ -414,6 +414,12 @@ int btrfs_get_raid_extent_offset(struct btrfs_fs_info *fs_info, int slot; int ret; + if (unlikely(!stripe_root)) { + btrfs_err_rl(fs_info, "missing raid stripe tree root for logical %llu", + logical); + return -EUCLEAN; + } + stripe_key.objectid = logical; stripe_key.type = BTRFS_RAID_STRIPE_KEY; stripe_key.offset = 0; From 330dcc553f282e8dc0b88c9495b4c296465364e1 Mon Sep 17 00:00:00 2001 From: Qu Wenruo Date: Sun, 12 Jul 2026 13:12:51 +0930 Subject: [PATCH 06/10] btrfs: raid56: fix an incorrect csum skip during scrub Commit 7425a2894019 ("btrfs: introduce btrfs_bio_for_each_block_all() helper") uses the new helper to replace the nested loop inside verify_bio_data_sectors(), which simplifies the code. However that also changed the behavior of "continue" when a block has no data checksum. Previously the "continue" would skip the old for() loop, which would also increase @total_sector_nr. Now the "continue" will skip the new btrfs_bio_for_each_block_all() loop, which doesn't update @total_sector_nr. This means if we hit a block that has no data checksum, we will skip all the remaining blocks no matter if they have data checksum. As @total_sector_nr will never be updated, and that test_bit() will always return false. Fix it by increasing @total_sector_nr before calling "continue". Fixes: 7425a2894019 ("btrfs: introduce btrfs_bio_for_each_block_all() helper") Reviewed-by: Daniel Vacek Signed-off-by: Qu Wenruo Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/raid56.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fs/btrfs/raid56.c b/fs/btrfs/raid56.c index f7f7db40994c..3f2896e793e3 100644 --- a/fs/btrfs/raid56.c +++ b/fs/btrfs/raid56.c @@ -1679,8 +1679,10 @@ static void verify_bio_data_sectors(struct btrfs_raid_bio *rbio, continue; /* No csum for this sector, skip to the next sector. */ - if (!test_bit(total_sector_nr, rbio->csum_bitmap)) + if (!test_bit(total_sector_nr, rbio->csum_bitmap)) { + total_sector_nr++; continue; + } expected_csum = rbio->csum_buf + total_sector_nr * fs_info->csum_size; btrfs_calculate_block_csum_pages(fs_info, paddrs, csum_buf); From 8bc4d7209611e8aa9d5409b6a4a86a9eb91b69a3 Mon Sep 17 00:00:00 2001 From: Guanghui Yang <3497809730@qq.com> Date: Tue, 14 Jul 2026 17:55:43 +0800 Subject: [PATCH 07/10] btrfs: zoned: fix missing chunk metadata reservation reserve_chunk_space() stores the return value of btrfs_zoned_activate_one_bg() in ret. The helper can return 1 after successfully activating a block group, but ret is later used to decide whether to reserve metadata for chunk tree updates. As a result, successful activation skips btrfs_block_rsv_add() and leaves trans->chunk_bytes_reserved unchanged. Use a separate variable for the activation result so positive success does not affect the later reservation. Keep activation failures in ret instead of returning early so the function uses the common tail path. Fixes: b6a98021e401 ("btrfs: zoned: activate necessary block group") CC: stable@vger.kernel.org Reviewed-by: Johannes Thumshirn Signed-off-by: Guanghui Yang <3497809730@qq.com> Signed-off-by: David Sterba --- fs/btrfs/block-group.c | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/fs/btrfs/block-group.c b/fs/btrfs/block-group.c index ab76a5173272..8def7abb728f 100644 --- a/fs/btrfs/block-group.c +++ b/fs/btrfs/block-group.c @@ -4532,25 +4532,29 @@ static void reserve_chunk_space(struct btrfs_trans_handle *trans, if (IS_ERR(bg)) { ret = PTR_ERR(bg); } else { + int activate_ret; + /* * We have a new chunk. We also need to activate it for * zoned filesystem. */ - ret = btrfs_zoned_activate_one_bg(info, true); - if (ret < 0) - return; - - /* - * If we fail to add the chunk item here, we end up - * trying again at phase 2 of chunk allocation, at - * btrfs_create_pending_block_groups(). So ignore - * any error here. An ENOSPC here could happen, due to - * the cases described at do_chunk_alloc() - the system - * block group we just created was just turned into RO - * mode by a scrub for example, or a running discard - * temporarily removed its free space entries, etc. - */ - btrfs_chunk_alloc_add_chunk_item(trans, bg); + activate_ret = btrfs_zoned_activate_one_bg(info, true); + if (activate_ret < 0) { + ret = activate_ret; + } else { + /* + * If we fail to add the chunk item here, we end + * up trying again at phase 2 of chunk allocation, + * at btrfs_create_pending_block_groups(). So + * ignore any error here. An ENOSPC here could + * happen, due to the cases described at + * do_chunk_alloc() - the system block group we + * just created was just turned into RO mode by a + * scrub for example, or a running discard + * temporarily removed its free space entries, etc. + */ + btrfs_chunk_alloc_add_chunk_item(trans, bg); + } } } From 0d214d14be503f16238999723acfcbf63f04cc8b Mon Sep 17 00:00:00 2001 From: "Paul E. McKenney" Date: Tue, 14 Jul 2026 13:32:24 -0700 Subject: [PATCH 08/10] btrfs: initialize 'args' to avoid compiler warning in btrfs_ioctl_get_csums() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [COMPILER WARNING] With GCC 11.5.0 and KASAN enabled on ARM, the following warning is triggered during compiling: In file included from ./include/asm-generic/rwonce.h:26, from ./arch/arm64/include/asm/rwonce.h:81, from ./include/linux/compiler.h:369, from ./include/linux/array_size.h:5, from ./include/linux/kernel.h:16, from fs/btrfs/ioctl.c:6: In function ‘instrument_copy_from_user_before’, inlined from ‘_inline_copy_from_user’ at ./include/linux/uaccess.h:184:2, inlined from ‘copy_from_user’ at ./include/linux/uaccess.h:222:9, inlined from ‘btrfs_ioctl_get_csums.isra’ at fs/btrfs/ioctl.c:5220:6: ./include/linux/kasan-checks.h:38:27: warning: ‘args’ may be used uninitialized [-Wmaybe-uninitialized] 38 | #define kasan_check_write __kasan_check_write ./include/linux/instrumented.h:146:9: note: in expansion of macro ‘kasan_check_write’ 146 | kasan_check_write(to, n); | ^~~~~~~~~~~~~~~~~ fs/btrfs/ioctl.c: In function ‘btrfs_ioctl_get_csums.isra’: ./include/linux/kasan-checks.h:20:6: note: by argument 1 of type ‘const volatile void *’ to ‘__kasan_check_write’ declared here 20 | bool __kasan_check_write(const volatile void *p, unsigned int size); | ^~~~~~~~~~~~~~~~~~~ fs/btrfs/ioctl.c:5201:43: note: ‘args’ declared here 5201 | struct btrfs_ioctl_get_csums_args args; | ^~~~ [POSSIBLE FALSE ALERTS] This seems to be a false alert from certain GCC versions. The @args is immediately over-written by copy_from_user(), and there is no code touching that @args until copy_from_user() finished correctly. [WORKAROUND] Initialize 'args' to zero, which suppresses the warning. Reviewed-by: Qu Wenruo Signed-off-by: Paul E. McKenney Reviewed-by: David Sterba Signed-off-by: David Sterba --- fs/btrfs/ioctl.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fs/btrfs/ioctl.c b/fs/btrfs/ioctl.c index 68b33f365fda..baa645e98812 100644 --- a/fs/btrfs/ioctl.c +++ b/fs/btrfs/ioctl.c @@ -5205,7 +5205,7 @@ static int btrfs_ioctl_get_csums(struct file *file, void __user *argp) struct btrfs_inode *inode = BTRFS_I(vfs_inode); struct btrfs_fs_info *fs_info = inode->root->fs_info; struct btrfs_root *root = inode->root; - struct btrfs_ioctl_get_csums_args args; + struct btrfs_ioctl_get_csums_args args = { 0 }; BTRFS_PATH_AUTO_FREE(path); const u64 ino = btrfs_ino(inode); const u32 csum_size = fs_info->csum_size; From ab602da96a915d42dcb1b0b322e8daea0f71b51f Mon Sep 17 00:00:00 2001 From: Johannes Thumshirn Date: Thu, 16 Jul 2026 10:23:12 +0200 Subject: [PATCH 09/10] btrfs: zoned: skip fully truncated ordered extents at zone finish A fully truncated ordered extent (truncated_len == 0) wrote no data, so its ->csum_list is empty and btrfs_finish_ordered_zoned() trips: assertion failed: !list_empty(&ordered->csum_list), in fs/btrfs/zoned.c:2141 Since commit 66ff4d366e7e a short or cancelled direct IO write finishes the unsubmitted ordered extent as truncated with uptodate = true instead of setting BTRFS_ORDERED_IOERR, so it now reaches btrfs_finish_ordered_zoned() rather than being skipped by the IOERR check in btrfs_finish_ordered_io(). generic/208 hits this on a zoned filesystem. Return early for these, like the BTRFS_ORDERED_PREALLOC case; there is no zone append result to record and btrfs_finish_one_ordered() skips them too. Fixes: 66ff4d366e7e ("btrfs: fix false IO failure after falling back to buffered write") Reviewed-by: Qu Wenruo Signed-off-by: Johannes Thumshirn Signed-off-by: David Sterba --- fs/btrfs/zoned.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/fs/btrfs/zoned.c b/fs/btrfs/zoned.c index 0706c0788cb2..a016cb471beb 100644 --- a/fs/btrfs/zoned.c +++ b/fs/btrfs/zoned.c @@ -2138,6 +2138,16 @@ void btrfs_finish_ordered_zoned(struct btrfs_ordered_extent *ordered) if (test_bit(BTRFS_ORDERED_PREALLOC, &ordered->flags)) return; + /* + * A fully truncated ordered extent wrote no data and so has + * no zone append result to record. + */ + if (test_bit(BTRFS_ORDERED_TRUNCATED, &ordered->flags) && + ordered->truncated_len == 0) { + ASSERT(list_empty(&ordered->csum_list)); + return; + } + ASSERT(!list_empty(&ordered->csum_list)); sum = list_first_entry(&ordered->csum_list, struct btrfs_ordered_sum, list); logical = sum->logical; From c4c0673e4cb15b0c127e6d00732a2427bdd12c11 Mon Sep 17 00:00:00 2001 From: Mykola Lysenko Date: Sat, 18 Jul 2026 16:37:10 -0700 Subject: [PATCH 10/10] btrfs: raid56: fix scrub read assembly submitting no reads Commit 5387bd958180 ("btrfs: raid56: remove sector_ptr structure") converted the bio-list membership checks from sector pointers to physical addresses. The two conversions in rmw_assemble_write_bios() kept their polarity (skip the sector when it is NOT in the bio list, i.e. when there is nothing to write), but scrub_assemble_read_bios() has the opposite polarity -- skip the sector when it IS in the bio list, because then there is nothing to read -- and the conversion flipped it: - sector = sector_in_rbio(rbio, stripe, sectornr, 1); - if (sector) + paddr = sector_paddr_in_rbio(rbio, stripe, sectornr, 1); + if (paddr == INVALID_PADDR) continue; Since a parity-scrub rbio's bio list only holds the empty completion bio, the result is that scrub_assemble_read_bios() submits no reads at all. finish_parity_scrub() then compares the parity it computes from the (cached, correct) data stripes against whatever happens to be in the freshly allocated, uninitialized stripe pages: - if the garbage differs from the computed parity, the sector is "repaired" and written back -- accidentally producing the correct on-disk result; - if a recycled page happens to still hold the old (correct) parity content, the sector is deemed clean, dropped from dbitmap, and the actually-corrupt on-disk parity is left in place. (Scrub reports no errors either way: there is no counter for P/Q corruption by design, so the bug here is purely the failure to read and repair.) The second case is intermittent because it depends on page-allocator recycling. Observed with fstests btrfs/297 (raid5, 2 devices): the corrupted P stripe intermittently stays corrupt after a scrub -- roughly 1/10 runs on x86-64 KVM and up to 7/8 on a UML build whose timing favors page reuse. Since the bio-list check can never be true for a parity-scrub rbio -- raid56_parity_alloc_scrub_rbio() adds a single empty completion bio (asserting bi_size == 0), bio_paddrs[] is only populated by index_rbio_pages() which is never called for BTRFS_RBIO_PARITY_SCRUB, and rbio_can_merge() refuses to merge rbios of different operations -- remove the dead check entirely and assert the invariant instead, as suggested by Qu Wenruo. After this fix the injected corruption is read, detected and repaired in every run (8/8 UML, 10/10 KVM), and the new assertion never fires across the full fstests raid group. Fixes: 5387bd958180 ("btrfs: raid56: remove sector_ptr structure") CC: stable@vger.kernel.org # 7.1+ Suggested-by: Qu Wenruo Assisted-by: Claude:claude-fable-5 Reviewed-by: Qu Wenruo Signed-off-by: Mykola Lysenko Signed-off-by: David Sterba --- fs/btrfs/raid56.c | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/fs/btrfs/raid56.c b/fs/btrfs/raid56.c index 3f2896e793e3..ca94c9e3d563 100644 --- a/fs/btrfs/raid56.c +++ b/fs/btrfs/raid56.c @@ -2911,13 +2911,12 @@ static int scrub_assemble_read_bios(struct btrfs_raid_bio *rbio) continue; /* - * We want to find all the sectors missing from the rbio and - * read them from the disk. If sector_paddr_in_rbio() finds a sector - * in the bio list we don't need to read it off the stripe. + * A parity-scrub rbio carries no data in its bio list: the + * only bio there is the empty completion bio added by + * raid56_parity_alloc_scrub_rbio(). Every sector is read + * from the stripe, so only assert that invariant here. */ - paddrs = sector_paddrs_in_rbio(rbio, stripe, sectornr, 1); - if (paddrs == NULL) - continue; + ASSERT(!sector_paddrs_in_rbio(rbio, stripe, sectornr, 1)); paddrs = rbio_stripe_paddrs(rbio, stripe, sectornr); /*