]> git.hungrycats.org Git - linux/commitdiff
btrfs: stripe_alloc: drop incremental claimable credit to end the over-count
authorZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Fri, 14 Aug 2026 00:04:59 +0000 (20:04 -0400)
committerZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Fri, 14 Aug 2026 00:04:59 +0000 (20:04 -0400)
The rule-1 credit in __btrfs_add_free_space credited whole stripes on every add,
but several add paths re-add free space that a low-level remove never
de-credited: the async-discard trim (unlink_free_space / bitmap_clear_bits, then
do_trimming re-adds via __btrfs_add_free_space) and btrfs_remove_free_space's
middle-split tail re-add.  The double-credit drives bg->stripe_claimable ABOVE
the authoritative per-commit scan (the "incremental stripe_claimable %llu above
scanned %llu" WARN) -- the over-count direction that is unsafe once admission
gates on the counter, where a reservation admitted against phantom supply is
dropped at writeback.

Drop the positive incremental credit entirely.  The incremental rules now only
ever DEBIT (removes round out, claims are exact -- both err low); the per-commit
rescan is the sole upward correction.  stripe_claimable can therefore only
under-count between commits (early, clean write()-time ENOSPC -- the
deliberately-safe direction) and never over-count, structurally, regardless of
which re-add path forgets to de-credit.

Reproduced with compress+autodefrag+discard=async on a legacy-converted raid5
fs (incremental ~= 2x scanned); credit_return attribution confirmed the phantom
entered through do_trimming's re-add, not the open-stripe allocator returns.

Assisted-by: Claude:claude-opus-4-8
fs/btrfs/free-space-cache.c

index 45adb7e9190087964790445554e83c0b988cce5d..1a66e798f220aadca719d232cdd97a72eb2549db 100644 (file)
@@ -2678,15 +2678,18 @@ link:
                kmem_cache_free(btrfs_free_space_cachep, info);
 out:
        /*
-        * Whole stripes wholly inside the returned range are trivially
-        * fully free and claimable, whatever merging did around them.
-        * Boundary fragments may complete stripes too; not detecting that
-        * errs claimable-low, and the rescan corrects it.
+        * Do NOT incrementally credit newly-freed whole stripes here.  Add paths
+        * re-add space that a low-level remove never de-credited -- the async
+        * discard trim (unlink_free_space / bitmap_clear_bits + do_trimming's
+        * re-add) and btrfs_remove_free_space's middle-split tail -- so crediting
+        * on add double-counts and stripe_claimable drifts ABOVE the authoritative
+        * scan.  That is the dangerous direction: once admission gates on the
+        * counter, an over-count admits reservations against phantom supply that
+        * writeback then drops.  The incremental rules now only ever DEBIT
+        * (removes/claims, which err low); the per-commit rescan is the sole
+        * upward correction, so the counter can only under-count (early, clean
+        * write()-time ENOSPC) -- never over-count.
         */
-       if (!ret && btrfs_is_stripe_alloc_bg(block_group))
-               stripe_claimable_mod(block_group,
-                                    stripe_interior_bytes(block_group,
-                                                          offset, bytes));
        btrfs_discard_update_discardable(block_group);
        spin_unlock(&ctl->tree_lock);
 
@@ -2796,8 +2799,12 @@ static void stripe_claimable_mod(struct btrfs_block_group *bg, s64 delta)
        spin_unlock(&sinfo->lock);
 }
 
-/* Bytes of whole stripes lying wholly inside [bytenr, bytenr + size). */
-static u64 stripe_interior_bytes(const struct btrfs_block_group *bg,
+/*
+ * Bytes of whole stripes lying wholly inside [bytenr, bytenr + size).  Unused
+ * since the incremental positive credit was dropped; kept for reference / a
+ * future credit rule that de-credits its removes symmetrically.
+ */
+static u64 __maybe_unused stripe_interior_bytes(const struct btrfs_block_group *bg,
                                 u64 bytenr, u64 size)
 {
        const u64 fsl = bg->full_stripe_len;