⬢ DragonFlyBSD Kernel Audit
DF-3559 / v1_neg.c
← back to finding ↓ download raw
/*
 * v1 riders for the pass-2 batch (files: sys/netgraph/echo/ng_echo.c,
 * sys/netgraph/hole/ng_hole.c):
 *
 *  R1  v1 echo data reflects to the ARRIVAL hook only (ng_echo.c:104);
 *      with a second (cross-wired) hook present, an injection on the
 *      entry hook comes back to the sender and NEVER propagates to the
 *      other hook -- the closed-ring-is-inert twin fact (ng7 echo_neg).
 *  R2  v1 echo control: ANY control message is bounced back to the
 *      sender as NGF_RESP (ng_echo.c:87-92); the v1 rcvmsg signature
 *      has NO hook parameter (netgraph.h:175) so no hook-addr relay
 *      (DF-3557 ng_UI class) is even expressible.
 *  R3  v1 hole eats all data (ng_hole.c:83).
 *  R4  v1 hole has NO rcvmsg (ng_hole.c:65 typestruct slot NULL) ->
 *      the DF-3558 ng7 getstats OOB read has no v1 twin: type-cookie'd
 *      messages die in the framework with EINVAL before any node code
 *      runs (ng_base.c:1195-1201), whether well-formed or under-sent
 *      (the exact DF-3558 trigger shape: arglen=32 advertised, ZERO
 *      argument bytes sent).
 *
 * v1 ABI (sys/netgraph/ng_message.h): NG_VERSION 2; header
 * version/spare:2 arglen:u16 flags:u32 token:u32 typecookie:u32
 * cmd:u32 cmdstr[32] = 48 bytes.  Path separator ':' (empty = self).
 *
 * Build: cc -O2 -o v1_neg v1_neg.c
 * Run (root, v1 netgraph.ko + ng_socket.ko + ng_echo.ko + ng_hole.ko).
 */

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.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		2
#define GEN_COOKIE		851672668UL	/* NGM_GENERIC_COOKIE (v1) */
#define ECHO_COOKIE		884298942UL	/* NGM_ECHO_COOKIE */
#define HOLE_COOKIE		915433206UL	/* NGM_HOLE_COOKIE */
#define NGM_MKPEER		2
#define NGM_NAME		4

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

struct ngm_hdr_ {
	u_char	version; u_char spare;
	u_short	arglen;			/* u_int16_t in v1! */
	u_int	flags, token, typecookie, cmd;
	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]; };
struct sockaddr_ng_ { u_char sg_len; u_char sg_family; char sg_data[64]; };

static u_int32_t tok = 0x5100;
static int pass = 0, fail = 0;

#define PASS(name) do { printf("PASS %s\n", name); pass++; } while (0)
#define FAIL(name, why) do { printf("FAIL %s: %s\n", name, why); fail++; } while (0)

static int
ngc_send(int cs, const char *path, u_long cookie, u_int cmd,
    const void *arg, size_t arglen)
{
	struct ng_mesg_ m;
	struct sockaddr_ng_ sa;

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

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

	return (sendto(cs, &m, sizeof(m.header) + arglen, 0,
	    (struct sockaddr *)&sa, sa.sg_len));
}

static int
wait_readable(int fd, int ms)
{
	struct timeval tv = { ms / 1000, (ms % 1000) * 1000 };
	fd_set r;
	FD_ZERO(&r);
	FD_SET(fd, &r);
	return (select(fd + 1, &r, NULL, NULL, &tv));
}

int
main(void)
{
	struct ngm_mkpeer_ mkp;
	struct ngm_name_ nm;
	struct sockaddr_ng_ sa, from;
	socklen_t fl;
	char buf[512];
	ssize_t n;
	int cs, ds, rc;

	printf("v1_neg: uid=%d (v1 twin-fact riders)\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));
		if (errno == EPERM)
			printf("RESULT unpriv-gated: v1 ngc_attach "
			    "caps_priv_check -> EPERM (root-only)\n");
		exit(0);
	}

	memset(&nm, 0, sizeof(nm));
	strcpy(nm.name, "vneg");
	if (ngc_send(cs, "", GEN_COOKIE, NGM_NAME, &nm, sizeof(nm)) < 0)
		printf("STEP name errno=%d (continuing)\n", errno);

	ds = socket(AF_NG, SOCK_DGRAM, NG_DATA);
	if (ds < 0) {
		printf("RESULT data-socket errno=%d\n", errno);
		exit(1);
	}
	/* v1: connect the data socket to the CONTROL node by absolute name
	 * (trailing ':' required -- bare "vneg" parses as a relative hook
	 * path and ng_path2node(NULL, ...) returns ENXIO) */
	memset(&sa, 0, sizeof(sa));
	sa.sg_family = AF_NG;
	strcpy(sa.sg_data, "vneg:");
	sa.sg_len = 2 + strlen(sa.sg_data) + 1;
	if (connect(ds, (struct sockaddr *)&sa, sa.sg_len) < 0) {
		printf("RESULT data-connect errno=%d (%s)\n", errno,
		    strerror(errno));
		exit(1);
	}
	printf("STEP sockets ready\n");
	fflush(stdout);

	/* R1: echo A under "e1"<->a; echo B cross-wired A.b<->B.c */
	memset(&mkp, 0, sizeof(mkp));
	strcpy(mkp.type, "echo"); strcpy(mkp.ourhook, "e1"); strcpy(mkp.peerhook, "a");
	if (ngc_send(cs, "", GEN_COOKIE, NGM_MKPEER, &mkp, sizeof(mkp)) < 0)
		{ printf("RESULT mkpeer-echoA errno=%d\n", errno); exit(1); }
	memset(&mkp, 0, sizeof(mkp));
	strcpy(mkp.type, "echo"); strcpy(mkp.ourhook, "b"); strcpy(mkp.peerhook, "c");
	if (ngc_send(cs, "e1", GEN_COOKIE, NGM_MKPEER, &mkp, sizeof(mkp)) < 0)
		{ printf("RESULT mkpeer-echoB errno=%d\n", errno); exit(1); }

	/* inject on the entry hook: must come straight back, never propagate */
	memset(&sa, 0, sizeof(sa));
	sa.sg_family = AF_NG;
	strcpy(sa.sg_data, "e1");
	sa.sg_len = 2 + strlen(sa.sg_data) + 1;
	n = sendto(ds, "R", 1, 0, (struct sockaddr *)&sa, sa.sg_len);
	if (n < 0)
		FAIL("R1-reflect", strerror(errno));
	else {
		rc = wait_readable(ds, 1500);
		if (rc > 0) {
			fl = sizeof(from);
			n = recvfrom(ds, buf, sizeof(buf), 0,
			    (struct sockaddr *)&from, &fl);
			if (n == 1 && buf[0] == 'R' &&
			    strcmp(from.sg_data, "e1") == 0)
				PASS("R1 echo reflects to arrival hook only "
				    "(1 byte back from 'e1')");
			else
				FAIL("R1-reflect", "wrong content/source");
		} else
			FAIL("R1-reflect", "no data returned");
	}
	/* nothing further may arrive (ring never entered) */
	rc = wait_readable(ds, 700);
	if (rc == 0)
		PASS("R1 cross-wired hook stays silent (ring inert)");
	else
		FAIL("R1-inert", "unexpected data");

	/* R2: ANY control message to echo bounces back to sender */
	rc = ngc_send(cs, "e1", ECHO_COOKIE, 0x1234, NULL, 0);
	if (rc < 0)
		FAIL("R2-bounce", strerror(errno));
	else {
		rc = wait_readable(cs, 1500);
		if (rc > 0) {
			fl = sizeof(from);
			n = recvfrom(cs, buf, sizeof(buf), 0,
			    (struct sockaddr *)&from, &fl);
			if (n >= (ssize_t)sizeof(struct ngm_hdr_)) {
				struct ng_mesg_ *rp = (struct ng_mesg_ *)buf;
				if (rp->header.flags & 0x1 /* NGF_RESP */)
					PASS("R2 echo bounces control msg "
					    "back as NGF_RESP (no relay)");
				else
					FAIL("R2-bounce", "reply lacks NGF_RESP");
			} else
				FAIL("R2-bounce", "short reply");
		} else
			FAIL("R2-bounce", "no reply");
	}

	/* R3 + R4: hole node */
	memset(&mkp, 0, sizeof(mkp));
	strcpy(mkp.type, "hole"); strcpy(mkp.ourhook, "h1"); strcpy(mkp.peerhook, "x");
	if (ngc_send(cs, "", GEN_COOKIE, NGM_MKPEER, &mkp, sizeof(mkp)) < 0)
		{ printf("RESULT mkpeer-hole errno=%d\n", errno); exit(1); }

	memset(&sa, 0, sizeof(sa));
	sa.sg_family = AF_NG;
	strcpy(sa.sg_data, "h1");
	sa.sg_len = 2 + strlen(sa.sg_data) + 1;
	n = sendto(ds, "D", 1, 0, (struct sockaddr *)&sa, sa.sg_len);
	if (n < 0)
		FAIL("R3-blackhole", strerror(errno));
	rc = wait_readable(ds, 700);
	if (rc == 0)
		PASS("R3 hole eats data (nothing returned)");
	else
		FAIL("R3-blackhole", "hole returned data?!");

	/* R4a: well-formed getstats (cmd 1 = NGM_HOLE_GET_STATS, arglen 32):
	 * ng7 would answer stats; v1 must EINVAL with no reply */
	errno = 0;
	{
		char arg[32];
		memset(arg, 0, sizeof(arg));
		rc = ngc_send(cs, "h1", HOLE_COOKIE, 1, arg, 32);
	}
	if (rc < 0 && errno == EINVAL)
		PASS("R4a v1 hole rejects GET_STATS with EINVAL "
		    "(no rcvmsg -> framework default)");
	else
		FAIL("R4a-getstats", rc < 0 ? strerror(errno) : "accepted?!");
	if (wait_readable(cs, 300) == 0)
		PASS("R4a no reply shipped back");
	else
		FAIL("R4a-noreply", "got a reply");

	/* R4b: the EXACT DF-3558 under-sent trigger -- header advertises
	 * arglen=32 but ZERO argument bytes are sent. v1 ngc_send has no
	 * arglen-vs-len validation (DF-0860) yet nothing reads msg->data:
	 * still EINVAL, no reply, no oracle. */
	errno = 0;
	rc = ngc_send(cs, "h1", HOLE_COOKIE, 1, NULL, 0); /* arglen header says 0 */
	/* now the raw under-sent variant: craft header claiming 32 */
	{
		struct ng_mesg_ m;
		struct sockaddr_ng_ sa2;
		memset(&m, 0, sizeof(m));
		m.header.version = NG_VERSION;
		m.header.arglen = 32;		/* LIE: no data follows */
		m.header.cmd = 1;		/* GET_STATS */
		m.header.token = ++tok;
		m.header.typecookie = HOLE_COOKIE;
		memset(&sa2, 0, sizeof(sa2));
		sa2.sg_family = AF_NG;
		strcpy(sa2.sg_data, "h1");
		sa2.sg_len = 2 + strlen(sa2.sg_data) + 1;
		errno = 0;
		rc = sendto(cs, &m, sizeof(m.header), 0,
		    (struct sockaddr *)&sa2, sa2.sg_len);	/* header only */
		if (rc < 0 && errno == EINVAL)
			PASS("R4b under-sent GET_STATS (arglen=32, 0 bytes "
			    "sent) -> EINVAL, twin of DF-3558 ABSENT");
		else
			FAIL("R4b-undersent", rc < 0 ? strerror(errno)
			    : "accepted?!");
		if (wait_readable(cs, 300) == 0)
			PASS("R4b no reply shipped (no OOB oracle)");
		else
			FAIL("R4b-noreply", "got a reply");
	}

	printf("RESULT riders pass=%d fail=%d\n", pass, fail);
	return (fail ? 1 : 0);
}