]> git.hungrycats.org Git - linux/commitdiff
btrfs: raid56: verify reconstructed tree blocks
authorZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Wed, 5 Aug 2026 06:59:50 +0000 (02:59 -0400)
committerZygo Blaxell <ce3g8jdj@umail.furryterror.org>
Wed, 16 Sep 2026 21:39:59 +0000 (17:39 -0400)
Data sectors recovered from parity are checked against the csum tree
before they are trusted: recover_vertical() calls verify_one_sector() for
each rebuilt sector, and a mismatch fails the read.  Metadata gets none of
that.  fill_data_csums() returns early for anything that is not a data
block group, so csum_bitmap is NULL, and verify_one_sector() then returns
success without looking at anything:

if (!rbio->csum_bitmap || !rbio->csum_buf)
return 0;

So a metadata rbio reconstructs a lost column, marks it uptodate having
verified nothing, and hands it back.  A wrong reconstruction is caught
much later by validate_extent_buffer(), one layer up, after the block has
already been returned and cached -- and in the raid6 case, where several
candidate reconstructions exist, raid56 cannot tell which one is right
because it has no way to check any of them.

A tree block can be verified, just not a sector at a time: its csum lives
in its own header and covers the whole nodesize block, so the check has to
wait until every sector of the block has been recovered.  Do it after the
vertical recovery loop rather than inside it.  A tree block never straddles
a column -- nodesize and BTRFS_STRIPE_LEN are both powers of two with
nodesize <= BTRFS_STRIPE_LEN, and full stripes are stripe-length aligned,
so each column starts on a nodesize boundary and tree blocks tile it
exactly.

Only the blocks the caller asked for are checked.  Those are known to be
live tree blocks; the rest of a stripe may be free space, which has no
header and would fail every test.  For the same reason this runs only when
rebuilding a read: recover_sectors() is also reached from the
read-modify-write path, where the sectors actually reconstructed are the
ones read off the surviving columns and any of them may be free space.  A
reconstruction feeding an RMW's parity therefore stays unverified; knowing
which ranges hold live metadata is not something this layer can do.

Tested with a 3-device raid5 filesystem with raid5 metadata, 3000 files:
each device in turn missing and corrupted recovers byte-exactly, and with
two devices corrupted -- where reconstruction necessarily produces a wrong
block -- raid56 now rejects it itself:

  raid56: reconstructed tree block 40796160 has bytenr 8993213254959039756,
  cannot be trusted

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

index c0921521216b8d9d7563c4fe6652809ad7db63a7..72474825c178508a70427a1a399a9f553caddb83 100644 (file)
@@ -13,6 +13,7 @@
 #include <linux/list_sort.h>
 #include <linux/raid/xor.h>
 #include <linux/mm.h>
+#include <crypto/hash.h>
 #include "messages.h"
 #include "ctree.h"
 #include "disk-io.h"
@@ -1853,6 +1854,172 @@ void raid56_parity_write(struct bio *bio, struct btrfs_io_context *bioc)
        start_async_work(rbio, rmw_rbio_work);
 }
 
+/*
+ * Return the memory holding a recovered sector: the bio's pages when we are
+ * rebuilding a read, the rbio's own stripe pages otherwise.
+ */
+static struct sector_ptr *recovered_sector(struct btrfs_raid_bio *rbio,
+                                          int stripe_nr, int sector_nr)
+{
+       if (rbio->operation == BTRFS_RBIO_READ_REBUILD)
+               return sector_in_rbio(rbio, stripe_nr, sector_nr, 0);
+       return rbio_stripe_sector(rbio, stripe_nr, sector_nr);
+}
+
+/*
+ * Verify one reconstructed tree block.
+ *
+ * Data sectors are verified individually against the csum tree, but a tree
+ * block carries its csum in its own header, covering the whole nodesize
+ * block.  So metadata cannot be checked a sector at a time: the check has to
+ * wait until every sector of the block has been recovered, which is why this
+ * runs after the vertical recovery loop rather than inside it.
+ *
+ * A tree block never straddles a column.  nodesize and BTRFS_STRIPE_LEN are
+ * both powers of two with nodesize <= BTRFS_STRIPE_LEN, and a full stripe is
+ * stripe-length aligned, so each column begins on a nodesize boundary and
+ * tree blocks tile it exactly.
+ *
+ * @sector_nr is the first sector of the block within the column.
+ */
+static int verify_one_eb(struct btrfs_raid_bio *rbio, int stripe_nr,
+                        int sector_nr)
+{
+       struct btrfs_fs_info *fs_info = rbio->bioc->fs_info;
+       const u32 sectorsize = fs_info->sectorsize;
+       const u32 sectors_per_tree = fs_info->nodesize >> fs_info->sectorsize_bits;
+       const u64 logical = rbio->bioc->full_stripe_logical +
+                           (u64)stripe_nr * BTRFS_STRIPE_LEN +
+                           (u64)sector_nr * sectorsize;
+       SHASH_DESC_ON_STACK(shash, fs_info->csum_shash);
+       u8 on_disk_csum[BTRFS_CSUM_SIZE];
+       u8 calculated_csum[BTRFS_CSUM_SIZE];
+       struct sector_ptr *sector;
+       void *kaddr;
+       u64 found_bytenr;
+       bool bad_fsid;
+
+       /*
+        * The header lives at the start of the block.  Read what we need from
+        * it before walking the rest: a block whose bytenr or fsid do not
+        * match was not reconstructed into what it claims to be, and there is
+        * no point csumming it.
+        */
+       sector = recovered_sector(rbio, stripe_nr, sector_nr);
+       kaddr = kmap_local_sector(sector);
+       memcpy(on_disk_csum, ((struct btrfs_header *)kaddr)->csum,
+              fs_info->csum_size);
+       found_bytenr = btrfs_stack_header_bytenr((struct btrfs_header *)kaddr);
+       bad_fsid = memcmp(((struct btrfs_header *)kaddr)->fsid,
+                         fs_info->fs_devices->metadata_uuid,
+                         BTRFS_FSID_SIZE) != 0;
+       kunmap_local(kaddr);
+
+       if (unlikely(found_bytenr != logical)) {
+               btrfs_warn_rl(fs_info,
+"raid56: reconstructed tree block %llu has bytenr %llu, cannot be trusted",
+                             logical, found_bytenr);
+               return -EIO;
+       }
+       if (unlikely(bad_fsid)) {
+               btrfs_warn_rl(fs_info,
+"raid56: reconstructed tree block %llu has the wrong fsid, cannot be trusted",
+                             logical);
+               return -EIO;
+       }
+
+       shash->tfm = fs_info->csum_shash;
+       crypto_shash_init(shash);
+       for (int i = 0; i < sectors_per_tree; i++) {
+               sector = recovered_sector(rbio, stripe_nr, sector_nr + i);
+               kaddr = kmap_local_sector(sector);
+               /* The csum field itself is not covered by the csum. */
+               if (i == 0)
+                       crypto_shash_update(shash, kaddr + BTRFS_CSUM_SIZE,
+                                           sectorsize - BTRFS_CSUM_SIZE);
+               else
+                       crypto_shash_update(shash, kaddr, sectorsize);
+               kunmap_local(kaddr);
+       }
+       crypto_shash_final(shash, calculated_csum);
+
+       if (unlikely(memcmp(calculated_csum, on_disk_csum, fs_info->csum_size))) {
+               btrfs_warn_rl(fs_info,
+"raid56: reconstructed tree block %llu fails its csum, cannot be trusted",
+                             logical);
+               return -EIO;
+       }
+       return 0;
+}
+
+/*
+ * Verify every tree block this rbio was asked to read, once recovery has
+ * filled in the missing columns.
+ *
+ * Only blocks the caller actually requested are checked: those are known to
+ * be live tree blocks, whereas the rest of a stripe may be free space, which
+ * has no header to verify and would fail every test here.
+ *
+ * Without this, a metadata rbio reconstructs a column and marks it uptodate
+ * having checked nothing -- verify_one_sector() returns success immediately
+ * when there is no csum_bitmap, and metadata never has one (see
+ * fill_data_csums()).  A wrong reconstruction would then be handed back to
+ * the caller, and cached, for validate_extent_buffer() to reject much later.
+ */
+static int verify_recovered_ebs(struct btrfs_raid_bio *rbio)
+{
+       struct btrfs_fs_info *fs_info = rbio->bioc->fs_info;
+       const u32 sectors_per_tree = fs_info->nodesize >> fs_info->sectorsize_bits;
+
+       /*
+        * Data has its own per-sector verification.  A mixed block group has
+        * neither: its sectors get no csums (fill_data_csums() skips mixed
+        * groups) and its contents may be file data rather than tree blocks,
+        * so there is nothing here to check either.
+        */
+       if (rbio->bioc->map_type & BTRFS_BLOCK_GROUP_DATA)
+               return 0;
+
+       /*
+        * Only when rebuilding a read.  recover_sectors() is also reached from
+        * the read-modify-write path, and there the bio list covers the blocks
+        * being written -- already-verified memory -- while the sectors that
+        * were actually reconstructed are the ones read off the other columns.
+        * Those cannot be checked: any of them may be free space rather than a
+        * live tree block, and free space has no header to verify.  Knowing
+        * which ranges hold live metadata is exactly what this layer lacks,
+        * so a reconstruction feeding an RMW's parity stays unverified.
+        */
+       if (rbio->operation != BTRFS_RBIO_READ_REBUILD)
+               return 0;
+
+       for (int stripe_nr = 0; stripe_nr < rbio->nr_data; stripe_nr++) {
+               for (int sector_nr = 0;
+                    sector_nr + sectors_per_tree <= rbio->stripe_nsectors;
+                    sector_nr += sectors_per_tree) {
+                       bool requested = true;
+                       int ret;
+
+                       for (int i = 0; i < sectors_per_tree; i++) {
+                               const int index = rbio_stripe_sector_index(rbio,
+                                               stripe_nr, sector_nr + i);
+
+                               if (!rbio->bio_sectors[index].has_paddr) {
+                                       requested = false;
+                                       break;
+                               }
+                       }
+                       if (!requested)
+                               continue;
+
+                       ret = verify_one_eb(rbio, stripe_nr, sector_nr);
+                       if (ret < 0)
+                               return ret;
+               }
+       }
+       return 0;
+}
+
 static int verify_one_sector(struct btrfs_raid_bio *rbio,
                             int stripe_nr, int sector_nr)
 {
@@ -2084,6 +2251,10 @@ static int recover_sectors(struct btrfs_raid_bio *rbio)
                        break;
        }
 
+       /* Metadata is verified per tree block, once every sector is back. */
+       if (ret == 0)
+               ret = verify_recovered_ebs(rbio);
+
 out:
        kfree(pointers);
        kfree(unmap_array);