From f103a31d370c7ffd372fdab12afa69d0b2b5951d Mon Sep 17 00:00:00 2001 From: Zygo Blaxell Date: Wed, 16 Sep 2026 00:47:30 -0400 Subject: [PATCH] btrfs: stripe_alloc: saturate the read-only guard's claimable subtraction The guard that keeps admitted bytes placeable when a group goes read-only subtracts the group's claimable bytes from the space_info's total. The two are not an atomic snapshot: stripe_claimable_mod() adds a delta to bg->stripe_claimable under the free-space tree lock and takes sinfo->lock only afterwards to add it to the aggregate, and inc_block_group_ro() holds sinfo->lock and the group's lock but not the tree lock, so it can read a group value that is ahead of the total. The unsigned subtraction then wraps to a huge supply, the guard admits the transition, and the group's stripes leave the supply under writers the gate already admitted -- the writeback allocation failure and data loss the guard exists to prevent. Saturate the subtraction. Found by review (2026-09-15), not by a test; the window is a few instructions wide, and the fill-edge and reclaim tests would report it only as a rare unexplained drop. Assisted-by: Claude:claude-fable-5-1 --- fs/btrfs/block-group.c | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/fs/btrfs/block-group.c b/fs/btrfs/block-group.c index c2db4e3a38819..6d450f0a98f98 100644 --- a/fs/btrfs/block-group.c +++ b/fs/btrfs/block-group.c @@ -3975,10 +3975,22 @@ static int inc_block_group_ro(struct btrfs_block_group *cache, bool force) * the bytes already admitted would no longer be placeable: * otherwise their writebacks fail and the data is dropped. */ - if (!ret && READ_ONCE(cache->stripe_unusable_ready) && - sinfo->bytes_may_use + sinfo->bytes_stripe_margin > - sinfo->bytes_stripe_claimable - cache->stripe_claimable) - ret = -ENOSPC; + if (!ret && READ_ONCE(cache->stripe_unusable_ready)) { + /* + * stripe_claimable_mod() updates the group's value + * under the free-space tree lock and only then the + * aggregate under sinfo->lock, so a reader here can see + * the group ahead of the total: saturate, or the + * difference wraps and waves the transition through. + */ + const u64 supply = sinfo->bytes_stripe_claimable; + const u64 mine = min(supply, + READ_ONCE(cache->stripe_claimable)); + + if (sinfo->bytes_may_use + sinfo->bytes_stripe_margin > + supply - mine) + ret = -ENOSPC; + } } else { /* * We overcommit metadata, so we need to do the -- 2.53.0