]> git.ipfire.org Git - thirdparty/linux.git/commitdiff
btrfs: raid56: fix scrub read assembly submitting no reads
authorMykola Lysenko <nickolay.lysenko@gmail.com>
Sat, 18 Jul 2026 23:37:10 +0000 (16:37 -0700)
committerDavid Sterba <dsterba@suse.com>
Tue, 21 Jul 2026 04:44:08 +0000 (06:44 +0200)
Commit 5387bd958180 ("btrfs: raid56: remove sector_ptr structure")
converted the bio-list membership checks from sector pointers to
physical addresses. The two conversions in rmw_assemble_write_bios()
kept their polarity (skip the sector when it is NOT in the bio list,
i.e. when there is nothing to write), but scrub_assemble_read_bios()
has the opposite polarity -- skip the sector when it IS in the bio
list, because then there is nothing to read -- and the conversion
flipped it:

- sector = sector_in_rbio(rbio, stripe, sectornr, 1);
- if (sector)
+ paddr = sector_paddr_in_rbio(rbio, stripe, sectornr, 1);
+ if (paddr == INVALID_PADDR)
continue;

Since a parity-scrub rbio's bio list only holds the empty completion
bio, the result is that scrub_assemble_read_bios() submits no reads at
all. finish_parity_scrub() then compares the parity it computes from
the (cached, correct) data stripes against whatever happens to be in
the freshly allocated, uninitialized stripe pages:

  - if the garbage differs from the computed parity, the sector is
    "repaired" and written back -- accidentally producing the correct
    on-disk result;

  - if a recycled page happens to still hold the old (correct) parity
    content, the sector is deemed clean, dropped from dbitmap, and the
    actually-corrupt on-disk parity is left in place. (Scrub reports
    no errors either way: there is no counter for P/Q corruption by
    design, so the bug here is purely the failure to read and repair.)

The second case is intermittent because it depends on page-allocator
recycling. Observed with fstests btrfs/297 (raid5, 2 devices): the
corrupted P stripe intermittently stays corrupt after a scrub --
roughly 1/10 runs on x86-64 KVM and up to 7/8 on a UML build whose
timing favors page reuse.

Since the bio-list check can never be true for a parity-scrub rbio --
raid56_parity_alloc_scrub_rbio() adds a single empty completion bio
(asserting bi_size == 0), bio_paddrs[] is only populated by
index_rbio_pages() which is never called for BTRFS_RBIO_PARITY_SCRUB,
and rbio_can_merge() refuses to merge rbios of different operations --
remove the dead check entirely and assert the invariant instead, as
suggested by Qu Wenruo.

After this fix the injected corruption is read, detected and repaired
in every run (8/8 UML, 10/10 KVM), and the new assertion never fires
across the full fstests raid group.

Fixes: 5387bd958180 ("btrfs: raid56: remove sector_ptr structure")
CC: stable@vger.kernel.org # 7.1+
Suggested-by: Qu Wenruo <quwenruo.btrfs@gmx.com>
Assisted-by: Claude:claude-fable-5
Reviewed-by: Qu Wenruo <wqu@suse.com>
Signed-off-by: Mykola Lysenko <nickolay.lysenko@gmail.com>
Signed-off-by: David Sterba <dsterba@suse.com>
fs/btrfs/raid56.c

index 3f2896e793e3a4bfba4d7a2a40cc9ca59514a83c..ca94c9e3d563386c17915f2f44ac26b9afd90f1a 100644 (file)
@@ -2911,13 +2911,12 @@ static int scrub_assemble_read_bios(struct btrfs_raid_bio *rbio)
                        continue;
 
                /*
-                * We want to find all the sectors missing from the rbio and
-                * read them from the disk. If sector_paddr_in_rbio() finds a sector
-                * in the bio list we don't need to read it off the stripe.
+                * A parity-scrub rbio carries no data in its bio list: the
+                * only bio there is the empty completion bio added by
+                * raid56_parity_alloc_scrub_rbio().  Every sector is read
+                * from the stripe, so only assert that invariant here.
                 */
-               paddrs = sector_paddrs_in_rbio(rbio, stripe, sectornr, 1);
-               if (paddrs == NULL)
-                       continue;
+               ASSERT(!sector_paddrs_in_rbio(rbio, stripe, sectornr, 1));
 
                paddrs = rbio_stripe_paddrs(rbio, stripe, sectornr);
                /*