From 9a66f201a0edcdba9145069be9d21d3e1f5ae53b Mon Sep 17 00:00:00 2001 From: Zygo Blaxell Date: Sun, 6 Sep 2026 01:14:43 -0400 Subject: [PATCH] btrfs: stripe_meta: do not steal from the global reserve below the relocation reserve With the whole-stripe reserve in place the fill stopped cleanly at the edge: 374 MiB claimable, the reserve plus margin plus the outstanding reservations, no canary, no abort. Then the test deleted half of its tiny files, and every unlink that the gate refused came back through the global reserve steal, which does not look at stripes. Deleting an inline file is pure COW churn under stripe_meta: it frees no stripe, since the leaf it lived in stays, and each transaction of unlinks takes fresh stripes. Metadata used did not move by a megabyte while the claimable supply went from 374 MiB to zero, and the next commit aborted with the canary ("claimable 0 open 1490944 trapped 1506082816"). Refuse the steal once the claimable supply is down to the relocation reserve plus the claim margin. The unlink fails with ENOSPC, which is recoverable, instead of the abort, which is not, and the reserve stays with the background relocation that is the only thing that turns trapped space back into whole stripes. Assisted-by: Claude:claude-fable-5 --- fs/btrfs/space-info.c | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/fs/btrfs/space-info.c b/fs/btrfs/space-info.c index f7fa2b6dd80e0..73f9d3c939c0c 100644 --- a/fs/btrfs/space-info.c +++ b/fs/btrfs/space-info.c @@ -1196,6 +1196,23 @@ static bool steal_from_global_rsv(struct btrfs_fs_info *fs_info, if (global_rsv->space_info != space_info) return false; + /* + * stripe_meta: the global reserve is refilled by fiat at every + * commit, so its bytes are only as real as the whole stripes behind + * them. Once the claimable supply is down to the relocation reserve, + * a stolen reservation admits a transaction that consumes fresh + * stripes through COW while everything it frees comes back trapped + * (an unlink of an inline file returns nothing at all), until a tree + * block finds no stripe and the commit aborts. Refuse the steal: + * -ENOSPC from an unlink is recoverable, an abort is not, and the + * reserve stays with the background relocation that will make room. + */ + if (space_info->bytes_stripe_reserve && + space_info->bytes_stripe_claimable < + space_info->bytes_stripe_reserve + space_info->bytes_stripe_margin + + ticket->bytes) + return false; + spin_lock(&global_rsv->lock); min_bytes = mult_perc(global_rsv->size, 10); if (global_rsv->reserved < min_bytes + ticket->bytes) { -- 2.53.0