]> git.hungrycats.org Git - linux/commitdiff
btrfs: stripe_alloc: never claim stripes covered by a live run
authorZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Wed, 29 Jul 2026 14:35:41 +0000 (10:35 -0400)
committerZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Wed, 16 Sep 2026 21:40:01 +0000 (17:40 -0400)
The range-to-run lookups -- attaching an ordered extent to its stripe run
and reporting completed data IO by bytenr -- assume that at most one run
on a block group's list covers any given address.  Nothing enforced that.

A reservation that is discarded before anything references it (the
cow_file_range error path under ENOSPC, the find_free_extent backout
paths) returns its bytes to the free space cache immediately, with no
pinning: there is no committed state to protect.  When an ENOSPC failure
storm discards every allocation in a stripe, the stripe is fully free
again and the claim rule -- correctly, by its own lights -- hands it out
as part of a new run while the old run object is still on the list
draining its other stripes' IO.  Full-stripe write batching widened a
run's post-close drain from microseconds to the parked-write timeout, and
the soak test hit the overlap within minutes: new allocations' ordered
extents attached to the old run (first match by range), their completions
drained the old run's inflight accounting into an assertion failure, and
the new run's accounting never drained, wedging the commit's retire wait.
The overlap is harmless to data -- a stripe can only be re-claimed if
every byte of it is free, and discarded reservations never issued bios --
but the accounting corruption is fatal.

Freed committed extents cannot reproduce this: they return to the free
cache only in the unpin phase at the tail of a commit, and the same
commit's retirement already drained -- and freed -- every run opened
before it.  Only the unpinned immediate-free paths race with a draining
run.

Rather than enumerate those paths, enforce the lookups' assumption at the
claim site: btrfs_claim_free_stripe_run() now trims a candidate to end
before the first live run overlapping it, or rejects it if its head
overlaps.  Re-claiming such stripes just waits until the old run drains
off the list, which only comes up inside ENOSPC failure storms.

Reproduced with concurrent fill-to-ENOSPC/delete cycles, balance, and
fsstress on a 4-device raid5: the assertion fired within ~15 minutes
unpatched, and ~10 hours of the same load ran clean with this fix.

Assisted-by: Claude:claude-fable-5
fs/btrfs/block-group.c
fs/btrfs/block-group.h
fs/btrfs/free-space-cache.c

index 3856e129a83085718802a74b2a9392e064e68440..fdc112fcc1f43d6004f66bd4112a90043d4457de 100644 (file)
@@ -742,6 +742,40 @@ out:
        return ret;
 }
 
+/*
+ * A live stripe run's address range must not be re-claimed while the run
+ * object still exists: an ENOSPC discard storm can return every byte of a
+ * stripe to the free cache while its run is still draining other stripes'
+ * IO, and a second run claimed over the same range would make the
+ * range-to-run lookups (ordered extent attach, IO reporting) ambiguous and
+ * corrupt the inflight accounting.  Trim the candidate to end before the
+ * first overlapping run; reject it outright if its head overlaps.  Caller
+ * holds ctl->tree_lock; stripe_run_lock nests inside it.
+ */
+bool btrfs_stripe_run_range_usable(struct btrfs_block_group *bg,
+                                  u64 run_start, u64 *run_len)
+{
+       struct btrfs_open_stripe_run *run;
+       unsigned long flags;
+       u64 len = *run_len;
+
+       spin_lock_irqsave(&bg->stripe_run_lock, flags);
+       list_for_each_entry(run, &bg->open_stripe_runs, list) {
+               if (run->end <= run_start || run->start >= run_start + len)
+                       continue;
+               if (run->start <= run_start) {
+                       len = 0;
+                       break;
+               }
+               len = run->start - run_start;
+       }
+       spin_unlock_irqrestore(&bg->stripe_run_lock, flags);
+       if (len < bg->full_stripe_len)
+               return false;
+       *run_len = len;
+       return true;
+}
+
 /*
  * Does @logical lie within a stripe run (open or draining)?  Used by the
  * raid56 layer to decide whether a partial write to this stripe may be
index ebcae80cde7f95014d4fba308153806e49ce5ecf..6bdb621e46a9d8157823613a57a30bd7257d480b 100644 (file)
@@ -393,6 +393,8 @@ struct btrfs_open_stripe_run *btrfs_get_open_stripe_run(
 void btrfs_close_bg_open_stripes(struct btrfs_block_group *bg);
 void btrfs_retire_block_group_stripes(struct btrfs_block_group *bg);
 bool btrfs_stripe_in_open_run(struct btrfs_fs_info *fs_info, u64 logical);
+bool btrfs_stripe_run_range_usable(struct btrfs_block_group *bg,
+                                  u64 run_start, u64 *run_len);
 void btrfs_retire_open_stripes(struct btrfs_fs_info *fs_info,
                               struct btrfs_transaction *trans);
 void btrfs_clear_data_reloc_bg(struct btrfs_block_group *bg);
index 872d0f225645c89f35fbf88d6edd9aeb3a130751..ce0cc70a195541c15d36f13ccc0cf7b96708756a 100644 (file)
@@ -3472,7 +3472,9 @@ int btrfs_claim_free_stripe_run(struct btrfs_block_group *block_group,
 
                if (entry->bitmap) {
                        if (find_stripe_run_in_bitmap(ctl, entry, want_bytes,
-                                                     &run_start, &run_len))
+                                                     &run_start, &run_len) &&
+                           btrfs_stripe_run_range_usable(block_group,
+                                                         run_start, &run_len))
                                goto found;
                        continue;
                }
@@ -3483,6 +3485,9 @@ int btrfs_claim_free_stripe_run(struct btrfs_block_group *block_group,
                run_len = min(want_bytes,
                              div64_u64(entry->offset + entry->bytes - run_start,
                                        fsl) * fsl);
+               if (!btrfs_stripe_run_range_usable(block_group, run_start,
+                                                  &run_len))
+                       continue;
                goto found;
        }
        goto out;