/*
 * DF-3536 -- netgraph7 ng_cisco: disconnecting the downstream hook with the
 *            10s keepalive ng_callout armed leaks the timer ng_item and its
 *            node reference (ng_cisco twin of DF-3531 / ng_tty).
 *
 * Root cause:
 *   cisco_newhook("downstream") arms the keepalive timer:
 *     sys/netgraph7/cisco/ng_cisco.c:219-220
 *       ng_callout(&sc->handle, node, NULL, hz*KEEPALIVE_SECS,
 *                  &cisco_keepalive, (void *)sc, 0);
 *   ng_callout() (netgraph7/netgraph/ng_base.c:3248-3268) allocates a
 *   struct ng_item carrying one NG_NODE_REF (ng_base.c:3257) and stores it
 *   as the callout argument.
 *   cisco_disconnect() cancels it:
 *     sys/netgraph7/cisco/ng_cisco.c:418
 *       ng_uncallout(&sc->handle, NG_HOOK_NODE(hook));
 *   ng_uncallout() (ng_base.c:3273-3296) frees the item only under
 *   `if ((rval > 0) && ...)`.  On this kernel callout_stop() of an ARMED,
 *   not-yet-fired callout completes the cancel inline but returns 0
 *   (DF-3515 kern_timeout contract violation: _callout_cancel_or_stop()
 *   early `return 0`, sys/kern/kern_timeout.c:886-890), so the gate never
 *   passes: the ng_item (with its NG_NODE_REF) is orphaned forever.
 *   The leaked reference pins the ng_node in the ID hash and pins the
 *   module: kldunload ng_cisco stays "Device busy"; repeating the cycle
 *   grows the M_NETGRAPH zone without bound.
 *
 * Cycle (leak mode):
 *   1. ng7 control socket (its node is auto-created)
 *   2. NGM_MKPEER type="cisco" ourhook="hN" peerhook="downstream"
 *        -> cisco_newhook -> ng_callout ARMED
 *   3. NGM_RMHOOK "hN"
 *        -> cisco_disconnect -> ng_uncallout -> item + node ref LEAKED
 *   4. close socket
 *
 * Control mode ("inet" arg): identical except peerhook="inet"
 * (no callout is armed for non-downstream hooks) -> allocation count must
 * stay flat.  The ONLY difference between leak and control runs is the
 * armed-then-cancelled ng_callout.
 *
 * Build:  cc -O2 -o ngcisco7_leak ngcisco7_leak.c
 * Run:    ./ngcisco7_leak <cycles> [inet]     (measure: vmstat -m | grep netgraph)
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

/* ---- ng7 netgraph user ABI (values from sys/netgraph7/ng_message.h) ---- */
#define AF_NG			32
#define NG_CONTROL		2
#define NG_VERSION		8
#define NGM_GENERIC_COOKIE	1137070366
#define NGM_MKPEER		2
#define NGM_RMHOOK		5

#define NG_TYPESIZ		32
#define NG_HOOKSIZ		32

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_rmhook_ {
	char	ourhook[NG_HOOKSIZ];
};
struct sockaddr_ng_ {
	u_char	sg_len;
	u_char	sg_family;	/* sa_family_t is __uint8_t on DragonFly */
	char	sg_data[64];
};

static u_int32_t tok = 0x7000;

static int
ng_send_generic(int cs, u_int cmd, const void *arg, size_t arglen)
{
	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 = NGM_GENERIC_COOKIE;
	if (arglen)
		memcpy(m.data, arg, arglen);

	memset(&sa, 0, sizeof(sa));
	sa.sg_family = AF_NG;
	sa.sg_data[0] = '\0';			/* own node */
	sa.sg_len = 2 + 1;

	n = sendto(cs, &m, sizeof(m.header) + arglen, 0,
	    (struct sockaddr *)&sa, sa.sg_len);
	if (n < 0)
		return (-1);
	/* MKPEER/RMHOOK have no reply; drain any stray async data */
	(void)recv(cs, &m, sizeof(m), MSG_DONTWAIT);
	return (0);
}

int
main(int argc, char **argv)
{
	long cycles = (argc > 1) ? atol(argv[1]) : 2000;
	int inet_mode = (argc > 2 && strcmp(argv[2], "inet") == 0);
	const char *peerhook = inet_mode ? "inet" : "downstream";
	long i;

	for (i = 0; i < cycles; i++) {
		int cs;
		struct ngm_mkpeer_ mk;
		struct ngm_rmhook_ rm;
		char hook[NG_HOOKSIZ];

		if ((cs = socket(AF_NG, SOCK_DGRAM, NG_CONTROL)) < 0) {
			perror("socket(AF_NETGRAPH, NG_CONTROL)");
			return (1);
		}

		/* mkpeer cisco downstream (or inet for the control run) */
		memset(&mk, 0, sizeof(mk));
		snprintf(mk.type, sizeof(mk.type), "cisco");
		snprintf(mk.ourhook, sizeof(mk.ourhook), "h%ld", i);
		snprintf(mk.peerhook, sizeof(mk.peerhook), "%s", peerhook);
		if (ng_send_generic(cs, NGM_MKPEER, &mk, sizeof(mk)) < 0) {
			fprintf(stderr, "cycle %ld: mkpeer: %s\n", i,
			    strerror(errno));
			close(cs);
			return (1);
		}

		/* immediately tear the hook down with the timer armed */
		memset(&rm, 0, sizeof(rm));
		snprintf(rm.ourhook, sizeof(rm.ourhook), "h%ld", i);
		if (ng_send_generic(cs, NGM_RMHOOK, &rm, sizeof(rm)) < 0) {
			fprintf(stderr, "cycle %ld: rmhook: %s\n", i,
			    strerror(errno));
			close(cs);
			return (1);
		}

		close(cs);

		if ((i % 500) == 499) {
			fprintf(stderr, "... %ld rounds (%s)\n", i + 1,
			    inet_mode ? "inet/control" : "downstream/leak");
		}
	}
	fprintf(stderr, "done: %ld rounds (%s)\n", cycles,
	    inet_mode ? "inet/control" : "downstream/leak");
	return (0);
}
