]> 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>
Sun, 6 Sep 2026 18:22:15 +0000 (14:22 -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 318721f2c28cf833b34edf59cf083ed8f5ed742f..09ca7dc4e933ae13db3342ea0d294048d576eaf6 100644 (file)
@@ -833,6 +833,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 59d5d04c02637b994d8f0e2b50c35d2d67dd3a3e..7ebeb08f77615d90370bd107acc32966c9bd5e20 100644 (file)
@@ -2875,6 +2875,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.
@@ -2951,10 +3011,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 a769aeb26c8ee59b6c631246c9df244c8de2f6e4..3ce0fe46640b47c2062e74618f164539a4a48178 100644 (file)
@@ -1250,6 +1250,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",
@@ -1270,6 +1271,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));