Re: [RFC 08/15] btrfs: implement RAID stripe-tree RAID5 writes

XIAO WU <[email protected]>
Newsgroups org.kernel.vger.linux-btrfs
Message-ID <[email protected]>
Hi Johannes,

I came across the Sashiko AI review [1] of this patch and was able to
reproduce a null pointer dereference during mount on a RAID5 filesystem
with the raid-stripe-tree feature enabled.  I wanted to share the
concrete evidence since it crashes deterministically with KASAN.

 > +    set->stripe_units[i].pstripe = pstripe;

The Sashik identified that btrfs_stripe_set_alloc() stores a
direct pointer into bioc->stripes[] without taking a reference on the
bioc.  When the data bios complete first, the bioc can be freed before
the async parity workqueue dereferences pstripe.  The review also noted
several other issues in the same patch:

- raid56_write_end_io_work() completes the data ordered extent before
   the parity bio finishes — an fsync could return success while data
   remains without parity protection on disk.

- btrfs_submit_raid56_write() overwrites bio->bi_end_io and may leak
   the base bioc allocation reference on every chunk write.

- btrfs_rst_raid56_write() folds the full unsplit bio size into the
   parity buffer, which could cause double-folding on the remainder
   when the caller loops after btrfs_split_bio().

- bio_set_dev(bio, pstripe->dev->bdev) followed by submit_bio()
   will panic on a degraded array where the missing device's bdev
   is NULL.

[Reproduction]

The PoC creates three loopback devices, makes a RAID5 btrfs, then
manually flips the raid-stripe-tree incompat flag in each superblock
(recalculating CRC32C) before mounting.  The null pointer dereference
occurs deterministically during mount in btrfs_read_block_groups().

[Crash log — kernel 7.1.0-g3b216e369cba, CONFIG_KASAN=y, SMP]

   BTRFS info (device loop0): first mount of filesystem df5f7a81-...
   BTRFS info (device loop0): using crc32c checksum algorithm

   Oops: general protection fault, probably for non-canonical address
   0xdffffc0000000020: 0000 [#1] SMP KASAN NOPTI
   KASAN: null-ptr-deref in range [0x0000000000000100-0x0000000000000107]

   RIP: 0010:btrfs_update_global_block_rsv+0x452/0x960
   RAX: 0000000000000020 RBX: 0000000000000000 RCX: 0000000000000000
   RDX: dffffc0000000000 RBP: 0000000000000000

   Call Trace:
    <TASK>
    btrfs_update_global_block_rsv+0x452/0x960
    btrfs_read_block_groups+0x16ee/0x2e20
    open_ctree+0x4804/0x8370
    btrfs_get_tree+0x1346/0x2740
    vfs_get_tree+0x93/0x340
    path_mount+0x7c4/0x2390
    __x64_sys_mount+0x298/0x310
    do_syscall_64+0x129/0x850
    entry_SYSCALL_64_after_hwframe+0x77/0x7f
    </TASK>

   Kernel panic - not syncing: Fatal exception

The PoC is attached below.  It compiles with:

   gcc -o poc poc.c -static

[1] 
https://sashiko.dev/#/patchset/20260619090211.497100-1-johannes.thumshirn%40wdc.com
     (Sashiko AI code review — "Use-After-Free", Severity: High)

Thanks,
XIAOWU

/*
  * PoC: btrfs RAID stripe-tree RAID5 write Use-After-Free
  *
  * Bug: In btrfs_stripe_set_alloc(), raw pointer into bioc->stripes[] is
  * stored as pstripe without incrementing bioc refcount.
  */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/mount.h>
#include <sys/wait.h>

#define MOUNT_POINT "/mnt"

int main(void)
{
     char cmd[1024];

     setbuf(stdout, NULL);

     system("losetup -D 2>/dev/null; umount /mnt 2>/dev/null");

     printf("[*] Creating backing files...\n");
     for (int i = 0; i < 3; i++) {
         snprintf(cmd, sizeof(cmd),
             "dd if=/dev/zero of=/tmp/disk%d.img bs=1M count=512 
2>/dev/null", i);
         system(cmd);
     }

     printf("[*] Setting up loop devices...\n");
     for (int i = 0; i < 3; i++) {
         snprintf(cmd, sizeof(cmd), "losetup /dev/loop%d /tmp/disk%d.img 
2>&1", i, i);
         system(cmd);
     }

     printf("[*] Creating RAID5 btrfs...\n");
     system("mkfs.btrfs -f -d raid5 -m raid5 /dev/loop0 /dev/loop1 
/dev/loop2 2>&1");
     system("sync");

     /*
      * Write Python script for superblock update to a file first
      * to avoid quoting issues.
      */
     printf("[*] Writing and running Python fix script...\n");
     system("cat > /tmp/fix_sb.py << 'SCRIPT'\n"
         "#!/usr/bin/env python3\n"
         "import struct, os, sys\n"
         "\n"
         "# CRC32C using polynomial 0x1EDC6F41 (reflected: 0x82F63B78)\n"
         "table = [0] * 256\n"
         "for i in range(256):\n"
         "    crc = i\n"
         "    for _ in range(8):\n"
         "        if crc & 1:\n"
         "            crc = (crc >> 1) ^ 0x82F63B78\n"
         "        else:\n"
         "            crc >>= 1\n"
         "    table[i] = crc\n"
         "\n"
         "def crc32c_calc(data):\n"
         "    crc = 0xFFFFFFFF\n"
         "    for byte in data:\n"
         "        crc = (crc >> 8) ^ table[(crc ^ byte) & 0xFF]\n"
         "    return crc ^ 0xFFFFFFFF\n"
         "\n"
         "# Verify with known test vector\n"
         "test_csum = crc32c_calc(b'\\x00' * 4064)  # 4096-32=4064\n"
         "print(f'Test CRC32C of 4064 zero bytes: {test_csum:08x}')\n"
         "\n"
         "for dev in ['/dev/loop0', '/dev/loop1', '/dev/loop2']:\n"
         "    for sb_base in [65536, 67108864]:\n"
         "        try:\n"
         "            with open(dev, 'r+b') as f:\n"
         "                f.seek(sb_base)\n"
         "                sb = bytearray(f.read(4096))\n"
         "                \n"
         "                # incompat_flags at offset 0xBC (188)\n"
         "                val = struct.unpack('<Q', sb[188:196])[0]\n"
         "                new_val = val | (1 << 14)\n"
         "                print(f'{dev}@{sb_base}: 0x{val:x} -> 
0x{new_val:x}')\n"
         "                struct.pack_into('<Q', sb, 188, new_val)\n"
         "                \n"
         "                # Compute CRC32C over bytes 32..4095\n"
         "                csum = crc32c_calc(bytes(sb[32:4096]))\n"
         "                struct.pack_into('<I', sb, 0, csum)\n"
         "                \n"
         "                f.seek(sb_base)\n"
         "                f.write(bytes(sb))\n"
         "        except Exception as e:\n"
         "            print(f'Error: {e}', file=sys.stderr)\n"
         "os.sync()\n"
         "print('Done!')\n"
         "SCRIPT\n");

     system("python3 /tmp/fix_sb.py 2>&1");

     printf("[*] Attempting mount ro,rescue=ignorebadroots...\n");
     if (mount("/dev/loop0", MOUNT_POINT, "btrfs", MS_RDONLY,
           "rescue=ignorebadroots,rescue=usebackuproot") != 0) {
         perror("mount with rescue");
         /* Try without usebackuproot */
         if (mount("/dev/loop0", MOUNT_POINT, "btrfs", MS_RDONLY,
               "rescue=ignorebadroots") != 0) {
             perror("mount simple rescue");
             system("dmesg | tail -5");
             goto cleanup;
         }
     }
     printf("[+] Mounted ro!\n");

     /* Try to remount rw */
     if (mount("/dev/loop0", MOUNT_POINT, "btrfs", MS_REMOUNT, NULL) == 0) {
         printf("[+] Remounted rw!\n");
         unsigned char *buf = malloc(262144);
         if (buf) {
             memset(buf, 0xA5, 262144);
             int fd = open(MOUNT_POINT "/test", O_CREAT | O_RDWR, 0644);
             if (fd >= 0) {
                 for (int i = 0; i < 50; i++) {
                     write(fd, buf, 262144);
                     fsync(fd);
                 }
                 close(fd);
             }
             free(buf);
         }
         sync();
         usleep(500000);
     }
     umount(MOUNT_POINT);

cleanup:
     system("losetup -D 2>/dev/null");
     for (int i = 0; i < 3; i++) {
         snprintf(cmd, sizeof(cmd), "rm -f /tmp/disk%d.img", i);
         system(cmd);
     }
     return 0;
}
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.