]> git.hungrycats.org Git - linux/commitdiff
btrfs: stripe_alloc: tell a dead tree block from a live one in the metadata RMW audit
authorZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Sun, 6 Sep 2026 13:01:40 +0000 (09:01 -0400)
committerZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Fri, 18 Sep 2026 21:36:28 +0000 (17:36 -0400)
The audit reports any older-generation header under a read-modify-write
as a write hole, and counted over six hundred per metadata fill test.
Since runs close at every transaction and freed stripes are reclaimed
whole, most such headers belong to blocks that died with their stripe:
the stripe was claimed fully free, the dead blocks kept their headers,
and a torn parity write over them damages nothing a reader can reach.

Ask the committed extent tree, the witness a degraded read after a
crash would consult, whether the block is still referenced.  Count and
report a live one as before (now with its bytenr and reference count),
and count a dead one as meta_rmw_ghost without a warning.

A sector the run has already re-allocated in the current transaction
carries an extent item of the current generation for the new block
while the old header is still on disk (the new write has not landed):
that is not a live block either, so the item generation must match the
header generation.  Without that check every single-stripe run of the
current transaction reported its dead predecessors as holes (90 of 135
warnings in one fill test; the other 45 were the raid5 system chunk).

Assisted-by: Claude:claude-fable-5
fs/btrfs/fs.h
fs/btrfs/raid56.c
fs/btrfs/sysfs.c

index f4c3226e10bc5607ab47614b1a3088c8cd615e86..318b29a358766b05335255eb6f529b701b77bc99 100644 (file)
@@ -887,6 +887,8 @@ struct btrfs_fs_info {
                 */
                atomic64_t meta_rmw_cur;
                atomic64_t meta_rmw_old;
+               atomic64_t meta_rmw_ghost;      /* older header, block dead in the
+                                                  committed extent tree */
                atomic64_t meta_rmw_free;
                atomic64_t congestion_short;
        } stripe_park_stats;
index 5306851cd94ad44076d69e35a462a0df20b98f8f..9f5eb5f6283d580d0f41536ac7db5b6d9aa30b04 100644 (file)
@@ -2996,6 +2996,66 @@ static bool rmw_peek_eb_generation(struct btrfs_raid_bio *rbio, u32 i,
        return found_bytenr == logical && !bad_fsid;
 }
 
+/*
+ * Is @bytenr a tree block the committed extent tree still references?  Read
+ * the commit root without locking, as the caching thread does, so this is
+ * safe from the raid56 worker with no transaction to join; a lookup that
+ * cannot answer counts the block as live.
+ */
+static bool rmw_block_referenced(struct btrfs_fs_info *fs_info, u64 bytenr,
+                                u64 header_gen, u64 *refs, u64 *extent_gen)
+{
+       struct btrfs_root *extent_root = btrfs_extent_root(fs_info, bytenr);
+       struct btrfs_path *path;
+       struct btrfs_key key;
+       bool live = true;
+       int ret;
+
+       *refs = 0;
+       *extent_gen = 0;
+       path = btrfs_alloc_path();
+       if (!path)
+               return true;
+       path->search_commit_root = 1;
+       path->skip_locking = 1;
+       key.objectid = bytenr;
+       key.type = BTRFS_METADATA_ITEM_KEY;
+       key.offset = (u64)-1;
+       down_read(&fs_info->commit_root_sem);
+       ret = btrfs_search_slot(NULL, extent_root, &key, path, 0, 0);
+       if (ret > 0)
+               ret = btrfs_previous_extent_item(extent_root, path, bytenr);
+       if (ret == 0) {
+               btrfs_item_key_to_cpu(path->nodes[0], &key, path->slots[0]);
+               if (key.objectid == bytenr &&
+                   (key.type == BTRFS_METADATA_ITEM_KEY ||
+                    key.type == BTRFS_EXTENT_ITEM_KEY)) {
+                       struct btrfs_extent_item *ei;
+
+                       ei = btrfs_item_ptr(path->nodes[0], path->slots[0],
+                                           struct btrfs_extent_item);
+                       *refs = btrfs_extent_refs(path->nodes[0], ei);
+                       *extent_gen = btrfs_extent_generation(path->nodes[0], ei);
+                       /*
+                        * An extent item at this bytenr from a LATER
+                        * transaction than the header on disk belongs to a
+                        * new block allocated here whose write has not landed
+                        * yet; the header is the dead previous occupant.
+                        * Only an item of the header's own generation makes
+                        * the block on disk live.
+                        */
+                       live = (*refs != 0 && *extent_gen == header_gen);
+               } else {
+                       live = false;
+               }
+       } else if (ret > 0) {
+               live = false;
+       }
+       up_read(&fs_info->commit_root_sem);
+       btrfs_free_path(path);
+       return live;
+}
+
 /*
  * Decide whether an uncovered read-modify-write is a write hole or only a
  * cost, by looking at what is actually in the stripe.
@@ -3072,10 +3132,36 @@ static void audit_uncovered_rmw(struct btrfs_raid_bio *rbio)
                        atomic64_inc(&fs_info->stripe_park_stats.meta_rmw_cur);
                        continue;
                }
-               atomic64_inc(&fs_info->stripe_park_stats.meta_rmw_old);
-               btrfs_warn_rl(fs_info,
-"read-modify-write of stripe %llu rewrites parity over committed tree block at generation %llu while writing generation %llu: write hole",
-                             rbio->bioc->full_stripe_logical, gen, write_gen);
+               /*
+                * An older header is only a write hole if the block is still
+                * referenced in the committed extent tree: a stripe reclaimed
+                * after every block in it died is claimed fully free and its
+                * dead blocks keep their headers, and a torn write over those
+                * damages nothing a reader can reach.  The committed tree is
+                * the witness a degraded read after a crash would consult, so
+                * ask it (no transaction: delayed refs of the running one are
+                * not yet what a crash would see).
+                */
+               {
+                       const int stripe_nr = i / rbio->stripe_nsectors;
+                       const int sectornr = i % rbio->stripe_nsectors;
+                       const u64 logical = rbio->bioc->full_stripe_logical +
+                               (u64)stripe_nr * BTRFS_STRIPE_LEN +
+                               (u64)sectornr * fs_info->sectorsize;
+                       u64 refs;
+                       u64 egen;
+
+                       if (!rmw_block_referenced(fs_info, logical, gen, &refs,
+                                                 &egen)) {
+                               atomic64_inc(&fs_info->stripe_park_stats.meta_rmw_ghost);
+                               continue;
+                       }
+                       atomic64_inc(&fs_info->stripe_park_stats.meta_rmw_old);
+                       btrfs_warn_rl(fs_info,
+"read-modify-write of stripe %llu rewrites parity over committed tree block %llu at generation %llu (refs %llu, extent generation %llu) while writing generation %llu: write hole",
+                                     rbio->bioc->full_stripe_logical, logical,
+                                     gen, refs, egen, write_gen);
+               }
        }
 }
 
index cf3d0049e5a3eda4c5d5c8a88d14eaaca018ac4c..6d5233e0ff5a13450e18f42d0a15082411e15863 100644 (file)
@@ -1140,6 +1140,7 @@ static ssize_t btrfs_stripe_park_stats_show(struct kobject *kobj,
                "meta_rmw %lld\n"
                "meta_rmw_cur %lld\n"
                "meta_rmw_old %lld\n"
+               "meta_rmw_ghost %lld\n"
                "meta_rmw_free %lld\n"
                "congestion_short %lld\n"
                "parked_now %d\n",
@@ -1160,6 +1161,7 @@ static ssize_t btrfs_stripe_park_stats_show(struct kobject *kobj,
                atomic64_read(&fs_info->stripe_park_stats.meta_rmw),
                atomic64_read(&fs_info->stripe_park_stats.meta_rmw_cur),
                atomic64_read(&fs_info->stripe_park_stats.meta_rmw_old),
+               atomic64_read(&fs_info->stripe_park_stats.meta_rmw_ghost),
                atomic64_read(&fs_info->stripe_park_stats.meta_rmw_free),
                atomic64_read(&fs_info->stripe_park_stats.congestion_short),
                atomic_read(&fs_info->stripe_parked_now));