]> git.hungrycats.org Git - linux/commitdiff
btrfs: add a write-hole invariant checker to the raid56 write path
authorZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Sat, 25 Jul 2026 15:11:02 +0000 (11:11 -0400)
committerZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Wed, 16 Sep 2026 21:40:00 +0000 (17:40 -0400)
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
fs/btrfs/block-group.c
fs/btrfs/block-group.h
fs/btrfs/extent-tree.c
fs/btrfs/raid56.c

index a4a98c9f8b2a4978d39fdd53d88f3f496c7f1951..fac0bad4b88ab8293451acc533d4e5f73fa5c864 100644 (file)
@@ -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
index 66c8f23139a6578081408ea722e75140833c220a..574371827cd79a34cd3dd9f7f581367f77afae8f 100644 (file)
@@ -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);
index ad29ad149067e3e5ceee77c22050a85899630fea..c3265fc2771d421c7963c1187ff35f55dfd3aa08 100644 (file)
@@ -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;
index 4d4852f2ba7eb50dabc354f95dcf016f1cbf3784..cabb7939e6787ff030cdb9fc2a266a4c43a94bdb 100644 (file)
@@ -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.