]> git.hungrycats.org Git - linux/commitdiff
btrfs: stripe_meta: carry raid56 metadata groups through a runtime enable or disable
authorZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Wed, 16 Sep 2026 04:47:30 +0000 (00:47 -0400)
committerZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Fri, 18 Sep 2026 21:36:30 +0000 (17:36 -0400)
The property path arms and disarms the trapped-space accounting of the
cached block groups, but stripe_alloc_sweep_groups() walked only DATA
space_infos: a runtime enable left already-cached raid56 metadata groups
unarmed until something else (a new chunk, a read-write transition)
initialised them, so their trapped and claimable bytes were missing from
metadata admission; a runtime disable left armed metadata groups' bytes
in the space_info after the option was gone.  Walk METADATA space_infos
too, for both directions.

Two more things a runtime toggle needs under raid56 metadata:

The metadata retirement at commit returned at once without the option,
so after a disable the running transaction's open metadata runs were
never closed or drained and their block group references were held
until unmount, where btrfs_free_block_groups() asserts on them.  Retire
unconditionally; the walk is over the list of groups with open runs and
is empty when there is nothing to do.

And the enable-time drain that keeps the write-hole check quiet until the
legacy allocator's writes have landed only waited for data (delalloc and
ordered extents).  Tree blocks the legacy allocator placed in the
transaction that was running at the flip are written by that
transaction's commit, into the partly used stripes they were allocated
in -- under raid56 metadata that commit is the last legacy
read-modify-write, and the metadata RMW reporter says so:

  read-modify-write of stripe 42860544 rewrites parity over committed
  tree block 42860544 at generation 10 ... while writing generation 11:
  write hole
  btrfs: sub-stripe write to stripe 42860544 (block group 34603008)
  outside any live stripe run: write hole window violated
  WARNING: CPU: 1 PID: 408294 at fs/btrfs/block-group.c:2889
  btrfs_stripe_check_write+0x109/0x150

Commit that transaction from the drain worker before arming the check.
The exposure of the enabling transaction itself is inherent -- its
blocks are already placed -- and bounded to that one commit.

Found by stripe-meta-toggle-test.sh (runtime enable with a fill to the
metadata edge, then a property-enabled filesystem disabled and
unmounted), on raid5 and raid6 metadata.

Assisted-by: Claude:claude-fable-5-1
fs/btrfs/block-group.c

index 0b3d873b5197e5337ad7c482d1a998ddc9a0acda..d86619188687898dbf3fd162eca55ffc932bcbbe 100644 (file)
@@ -2083,8 +2083,9 @@ int btrfs_stripe_alloc_check_support(struct btrfs_fs_info *fs_info)
 }
 
 /*
- * Arm or disarm stripe_unusable accounting across all raid56 data block
- * groups whose caches are loaded.  Same walk as the commit-time rescan.
+ * Arm or disarm stripe_unusable accounting across all raid56 data and
+ * metadata block groups whose caches are loaded.  Same walk as the
+ * commit-time rescan.
  */
 static void stripe_alloc_sweep_groups(struct btrfs_fs_info *fs_info, bool arm)
 {
@@ -2093,20 +2094,25 @@ static void stripe_alloc_sweep_groups(struct btrfs_fs_info *fs_info, bool arm)
        list_for_each_entry(sinfo, &fs_info->space_info, list) {
                int raid;
 
-               if (!(sinfo->flags & BTRFS_BLOCK_GROUP_DATA)) {
-                       /*
-                        * Metadata: the scan that maintains the stripe_meta
-                        * margin and reserve stops with the option, so clear
-                        * them here or they hold metadata back forever.
-                        */
-                       if (!arm && (sinfo->flags & BTRFS_BLOCK_GROUP_METADATA)) {
-                               spin_lock(&sinfo->lock);
-                               sinfo->bytes_stripe_margin = 0;
-                               sinfo->bytes_stripe_reserve = 0;
-                               btrfs_try_granting_tickets(sinfo);
-                               spin_unlock(&sinfo->lock);
-                       }
+               if (!(sinfo->flags & (BTRFS_BLOCK_GROUP_DATA |
+                                     BTRFS_BLOCK_GROUP_METADATA)))
                        continue;
+               /*
+                * Metadata: the scan that maintains the stripe_meta margin
+                * and reserve stops with the option, so clear them here or
+                * they hold metadata back forever.  The groups themselves
+                * are armed and disarmed below like data groups: raid56
+                * metadata places tree blocks in whole stripes from the
+                * moment the option is on, so its trapped and claimable
+                * bytes count from that moment and stop counting when it
+                * goes off.
+                */
+               if (!arm && (sinfo->flags & BTRFS_BLOCK_GROUP_METADATA)) {
+                       spin_lock(&sinfo->lock);
+                       sinfo->bytes_stripe_margin = 0;
+                       sinfo->bytes_stripe_reserve = 0;
+                       btrfs_try_granting_tickets(sinfo);
+                       spin_unlock(&sinfo->lock);
                }
                down_read(&sinfo->groups_sem);
                for (raid = 0; raid < BTRFS_NR_RAID_TYPES; raid++) {
@@ -2146,6 +2152,15 @@ void btrfs_stripe_alloc_enable_work(struct work_struct *work)
 
        btrfs_start_delalloc_roots(fs_info, LONG_MAX, false);
        btrfs_wait_ordered_roots(fs_info, U64_MAX, NULL);
+       /*
+        * Tree blocks the legacy allocator placed in the transaction that
+        * was running at the flip are written by that transaction's commit,
+        * into the partly used stripes they were allocated in: under raid56
+        * metadata that commit is the last legacy read-modify-write, and the
+        * metadata RMW reporter says so.  Wait for it before arming the
+        * check, which would otherwise report those writes as violations.
+        */
+       btrfs_commit_current_transaction(fs_info->tree_root);
        clear_bit(BTRFS_FS_STRIPE_ALLOC_ENABLING, &fs_info->flags);
        btrfs_info(fs_info,
           "stripe_alloc: writes placed before the policy was enabled have landed");
@@ -2533,9 +2548,13 @@ void btrfs_retire_meta_stripes(struct btrfs_fs_info *fs_info, u64 transid)
        LIST_HEAD(retire_list);
        unsigned long flags;
 
-       if (!btrfs_test_opt(fs_info, STRIPE_ALLOC))
-               return;
-
+       /*
+        * No option test: a runtime disable leaves the running transaction's
+        * metadata runs open, and the commit that follows still has to close
+        * and drain them or their block groups never drop their references.
+        * The list is empty when nothing is open, so this costs nothing on a
+        * filesystem that never had the policy.
+        */
        spin_lock(&fs_info->open_stripe_lock);
        list_for_each_entry(bg, &fs_info->open_stripe_bgs, open_stripe_bg_list)
                list_add_tail(&bg->open_stripe_retire_list, &retire_list);