From: Zygo Blaxell Date: Sat, 25 Jul 2026 15:11:02 +0000 (-0400) Subject: btrfs: add a write-hole invariant checker to the raid56 write path X-Git-Url: http://git.hungrycats.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=298233e996fde46d5c9bdafe05b1550b4c1fe232;p=linux btrfs: add a write-hole invariant checker to the raid56 write path With stripe-exclusive allocation, a raid56 data stripe may only be written while an open or draining stripe run covers it: after its run retires and drains at a transaction commit, nothing may ever write to it again, and a write outside any run means an allocation bypassed the policy. Both cases are the write hole about to happen. Check the invariant (under CONFIG_BTRFS_DEBUG) for every raid56 write operation, full-stripe and sub-stripe alike, at rmw_rbio() time. This turns every upstream violation -- a missed allocation path, a retirement ordering bug, an accounting leak -- into a deterministic WARN at the moment of the offending write, instead of silent damage that needs a crash plus a device failure plus a scrub to observe. The bios gathered in an rbio have not reported their IO done yet, so their runs cannot drain under the check: no false positives from completion races. Block groups that ever hosted relocation-class runs are skipped (sticky, debug-only flag): relocation legitimately overwrites its preallocated extents in place after their runs drain, and the write path cannot tell those writes from violations. Assisted-by: Claude:claude-fable-5 --- diff --git a/fs/btrfs/block-group.c b/fs/btrfs/block-group.c index a4a98c9f8b2a4..fac0bad4b88ab 100644 --- a/fs/btrfs/block-group.c +++ b/fs/btrfs/block-group.c @@ -880,6 +880,57 @@ bool btrfs_stripe_alloc_forces_cow(struct btrfs_fs_info *fs_info, u64 bytenr) return ret; } +#ifdef CONFIG_BTRFS_DEBUG +/* + * Write-hole invariant checker, called for every raid56 write operation + * (full-stripe and sub-stripe/RMW alike). With stripe-exclusive + * allocation, a raid56 data stripe may only be written while an open or + * draining stripe run covers it: once its run retires and drains, nothing + * may ever write it again, and a write outside any run means an + * allocation bypassed the policy. Either way it is the write hole about + * to happen, caught deterministically at the point of the write instead + * of probabilistically after a crash plus a device failure. + * + * Block groups that hosted relocation-class runs are skipped: + * relocation legitimately overwrites its preallocated extents in place + * after their runs drain. + */ +void btrfs_stripe_check_write(struct btrfs_fs_info *fs_info, + u64 full_stripe_start, bool sub_stripe) +{ + struct btrfs_open_stripe_run *run; + struct btrfs_block_group *bg; + unsigned long flags; + bool live = false; + u64 fsl; + + if (!btrfs_test_opt(fs_info, STRIPE_ALLOC)) + return; + bg = btrfs_lookup_block_group(fs_info, full_stripe_start); + if (!bg) + return; + if (!btrfs_is_stripe_alloc_bg(bg) || + test_bit(BLOCK_GROUP_FLAG_STRIPE_RELOC_USED, &bg->runtime_flags)) + goto out; + fsl = bg->full_stripe_len; + spin_lock_irqsave(&bg->stripe_run_lock, flags); + list_for_each_entry(run, &bg->open_stripe_runs, list) { + if (full_stripe_start < run->end && + full_stripe_start + fsl > run->start) { + live = true; + break; + } + } + spin_unlock_irqrestore(&bg->stripe_run_lock, flags); + WARN_ONCE(!live, +"btrfs: %s write to stripe %llu (block group %llu) outside any live stripe run: write hole window violated", + sub_stripe ? "sub-stripe" : "full-stripe", + full_stripe_start, bg->start); +out: + btrfs_put_block_group(bg); +} +#endif + /* * Drop the dedication of a block group to data relocation. Shared by the * zoned allocator and the stripe allocation policy; both dedicate one diff --git a/fs/btrfs/block-group.h b/fs/btrfs/block-group.h index 66c8f23139a65..574371827cd79 100644 --- a/fs/btrfs/block-group.h +++ b/fs/btrfs/block-group.h @@ -105,6 +105,14 @@ enum btrfs_block_group_flags { * transaction. */ BLOCK_GROUP_FLAG_NEW, + BLOCK_GROUP_FLAG_STRIPE_REMOVAL_PENDING, + /* + * The group has hosted relocation-class stripe runs. Relocation + * overwrites its preallocated extents in place after their runs + * drain, so the write-hole debug check cannot tell those writes + * from violations and skips such groups (sticky, debug only). + */ + BLOCK_GROUP_FLAG_STRIPE_RELOC_USED, }; enum btrfs_caching_type { @@ -361,6 +369,14 @@ void btrfs_clear_data_reloc_bg(struct btrfs_block_group *bg); void btrfs_release_data_reloc_bg(struct btrfs_fs_info *fs_info); bool btrfs_stripe_alloc_forces_cow(struct btrfs_fs_info *fs_info, u64 bytenr); bool btrfs_is_stripe_alloc_bg(const struct btrfs_block_group *bg); +#ifdef CONFIG_BTRFS_DEBUG +void btrfs_stripe_check_write(struct btrfs_fs_info *fs_info, + u64 full_stripe_start, bool sub_stripe); +#else +static inline void btrfs_stripe_check_write(struct btrfs_fs_info *fs_info, + u64 full_stripe_start, + bool sub_stripe) { } +#endif struct btrfs_block_group *btrfs_inc_nocow_writers(struct btrfs_fs_info *fs_info, u64 bytenr); void btrfs_dec_nocow_writers(struct btrfs_block_group *bg); diff --git a/fs/btrfs/extent-tree.c b/fs/btrfs/extent-tree.c index ad29ad149067e..c3265fc2771d4 100644 --- a/fs/btrfs/extent-tree.c +++ b/fs/btrfs/extent-tree.c @@ -4068,6 +4068,9 @@ static int do_allocation_stripe(struct btrfs_block_group *block_group, btrfs_clear_data_reloc_bg(block_group); return 1; } + if (class == BTRFS_STRIPE_RUN_RELOC) + set_bit(BLOCK_GROUP_FLAG_STRIPE_RELOC_USED, + &block_group->runtime_flags); ffe_ctl->found_offset = offset; ffe_ctl->search_start = offset; return 0; diff --git a/fs/btrfs/raid56.c b/fs/btrfs/raid56.c index 4d4852f2ba7eb..cabb7939e6787 100644 --- a/fs/btrfs/raid56.c +++ b/fs/btrfs/raid56.c @@ -21,6 +21,7 @@ #include "async-thread.h" #include "file-item.h" #include "btrfs_inode.h" +#include "block-group.h" /* set when additional merges to this rbio are not allowed */ #define RBIO_RMW_LOCKED_BIT 1 @@ -2331,6 +2332,16 @@ static void rmw_rbio(struct btrfs_raid_bio *rbio) int sectornr; int ret = 0; + /* + * Write-hole invariant check: every write must land in a stripe + * covered by a live stripe run when stripe-exclusive allocation is + * enabled. The bios gathered in this rbio have not reported their + * IO done yet, so their runs cannot drain under us. + */ + btrfs_stripe_check_write(rbio->bioc->fs_info, + rbio->bioc->full_stripe_logical, + !rbio_is_full(rbio)); + /* * Allocate the pages for parity first, as P/Q pages will always be * needed for both full-stripe and sub-stripe writes.