/*
 * DF-3531 -- netgraph7 ng_tty: armed-then-cancelled chand callout leaks the
 *            ng_item + node reference on tty close (ng_tty twin of
 *            DF-3515/DF-3523, same ng_uncallout defect)
 *
 * Root cause:
 *   ngt_start() (sys/netgraph7/tty/ng_tty.c:453-454) arms the drain timer
 *   via ng_callout(&sc->chand, sc->node, NULL, 1, ngt_timeout, ...), which
 *   allocates a struct ng_item holding one NG_NODE_REF
 *   (netgraph7/netgraph/ng_base.c:3248-3268).
 *   ngt_close() (ng_tty.c:252-253) cancels it with
 *   `if (callout_pending(&sc->chand)) ng_uncallout(&sc->chand, sc->node);`
 *   ng_uncallout() (ng_base.c:3273-3296) frees that item only when
 *   callout_stop() returns > 0 -- but on this kernel an ARMED, not-yet-fired
 *   stop is completed inline and callout_stop() returns 0 (the DF-3515
 *   kern_timeout contract violation), so the gate at ng_base.c:3284 never
 *   passes.  Even when it passes, NG_FREE_ITEM() is flag-only in ng7
 *   (netgraph.h:787-793) and ng_free_item() is never called.  Result:
 *   every tty close with a pending chand callout permanently leaks
 *   one ng_item + one node reference (the leaked ref pins the whole
 *   ng_node struct in the id hash and pins the module: kldunload stays
 *   "Device busy").
 *
 * Trigger per cycle (root: ngt_open needs SYSCAP_NONET_NETGRAPH):
 *   1. openpty (unix98: /dev/ptmx)
 *   2. TIOCSETD = NETGRAPHDISC(6) on the slave  -> ngt_open() creates
 *      the ng7 node "ttyN"; NGIOCGINFO gives us its name
 *   3. connect an ng7 data-socket hook to "ttyN:hook"
 *   4. send 1500 bytes; the pty master is never read, so t_outq hits
 *      NGT_HIWATER(400), the remainder sits in sc->outq and ngt_start()
 *      arms (and keeps re-arming every tick) sc->chand
 *   5. close(slave) -> ngt_close() -> callout_pending() != 0 ->
 *      ng_uncallout() -> item + node ref LEAKED
 *
 * Control run ("drain" arg): after sending, drain the master and sleep,
 * so outq empties, the callout fires once and does NOT re-arm -> at close
 * callout_pending() == 0 -> ng_uncallout() not called -> allocation count
 * must stay flat.  The only difference between leak and control runs is
 * the armed-then-cancelled callout.
 *
 * Build:  cc -O2 -o ngtty_leak ngtty_leak.c
 * Run:    ./ngtty_leak <cycles> [drain]      (measure with: vmstat -m | grep netgraph)
 */

#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>
#include <fcntl.h>

/* ---- ng7 netgraph user ABI (self-contained copies; values verified
 *      against sys/netgraph7/ng_message.h and the DF-3523 harness) ---- */
#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 NG_TYPESIZ		32
#define NG_HOOKSIZ		32
#define NG_NODESIZ		32
#define NG_PATHSIZ		512

#define NETGRAPHDISC_		6	/* sys/sys/ttycom.h: NETGRAPHDISC */

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_connect_ {
	char	path[NG_PATHSIZ];
	char	ourhook[NG_HOOKSIZ];
	char	peerhook[NG_HOOKSIZ];
};
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];
};

/* sys/netgraph7/ng_message.h: _IOR('N', 40, struct nodeinfo) */
#define NGIOCGINFO_		_IOR('N', 40, struct nodeinfo_)

static u_int32_t tok = 0x9000;

static int
ng_send_msg(int cs, const char *path, 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 = NGM_GENERIC_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;

	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;
			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);
			}
		}
		return (-1);
	}
	return (0);
}

/* one full leak cycle; returns 0 on success */
static int
cycle(int drain, int idx)
{
	int master, slave, cs, ds, disc = NETGRAPHDISC_;
	struct nodeinfo_ ni;
	struct ngm_connect_ con;
	struct sockaddr_ng_ sa;
	static char buf[1500];
	char path[64];
	ssize_t n;

	memset(&ni, 0, sizeof(ni));

	/* 1. unix98 pty */
	if ((master = posix_openpt(O_RDWR | O_NOCTTY)) < 0) {
		perror("posix_openpt"); return (-1);
	}
	/* master reads must not block (control-mode drain loop) */
	fcntl(master, F_SETFL, O_NONBLOCK);
	if (grantpt(master) < 0 || unlockpt(master) < 0) {
		perror("grantpt/unlockpt"); return (-1);
	}
	if ((slave = open(ptsname(master), O_RDWR | O_NOCTTY)) < 0) {
		perror("open pty slave"); return (-1);
	}

	/* 2. install the ng7 netgraph tty line discipline */
	if (ioctl(slave, TIOCSETD, &disc) < 0) {
		perror("TIOCSETD NETGRAPHDISC"); return (-1);
	}
	if (ioctl(slave, NGIOCGINFO_, &ni) < 0) {
		perror("NGIOCGINFO"); return (-1);
	}
	if (strncmp(ni.type, "tty", 3) != 0) {
		fprintf(stderr, "cycle %d: node type is \"%s\" (not ng_tty!)\n",
		    idx, ni.type);
		return (-1);
	}

	/* 3. hook a data socket to "<nodename>:hook" */
	cs = socket(AF_NG, SOCK_DGRAM, NG_CONTROL);
	ds = socket(AF_NG, SOCK_DGRAM, NG_DATA);
	if (cs < 0 || ds < 0) { perror("ng sockets"); return (-1); }
	memset(&con, 0, sizeof(con));
	snprintf(con.path, sizeof(con.path), "%s:", ni.name);
	strcpy(con.ourhook, "d");
	strcpy(con.peerhook, "hook");
	if (ng_send_msg(cs, ".", NGM_CONNECT, &con, sizeof(con), NULL) < 0) {
		fprintf(stderr, "cycle %d: connect %s: %s\n", idx, con.path,
		    strerror(errno));
		return (-1);
	}
	{
		struct ng_mesg_ reply;
		struct nodeinfo_ *me;
		if (ng_send_msg(cs, ".", NGM_NODEINFO, NULL, 0, &reply) < 0) {
			perror("nodeinfo"); return (-1);
		}
		me = (struct nodeinfo_ *)reply.data;
		memset(&sa, 0, sizeof(sa));
		sa.sg_family = AF_NG;
		snprintf(sa.sg_data, sizeof(sa.sg_data), "[%x]:", me->id);
		sa.sg_len = 2 + strlen(sa.sg_data) + 1;
		if (connect(ds, (struct sockaddr *)&sa, sa.sg_len) < 0) {
			perror("connect data socket"); return (-1);
		}
	}

	/* 4. queue output the pty master will never read (leak mode) */
	memset(&sa, 0, sizeof(sa));
	sa.sg_family = AF_NG;
	strcpy(sa.sg_data, "d");
	sa.sg_len = 2 + strlen(sa.sg_data) + 1;
	n = sendto(ds, buf, sizeof(buf), 0, (struct sockaddr *)&sa, sa.sg_len);
	if (n != (ssize_t)sizeof(buf)) {
		fprintf(stderr, "cycle %d: sendto: %s\n", idx, strerror(errno));
		return (-1);
	}

	if (drain) {
		/* control: drain the master so outq empties and the callout
		 * fires once without re-arming */
		char dbuf[4096];
		usleep(30000);
		while (read(master, dbuf, sizeof(dbuf)) > 0)
			;
		usleep(30000);
		while (read(master, dbuf, sizeof(dbuf)) > 0)
			;
		usleep(30000);
	} else {
		/* leak: leave outq non-empty; chand re-arms every tick */
		usleep(20000);
	}

	/* 5. tear the line discipline down: the TIOCSETD handler calls
	 *    l_close (ngt_close) synchronously (sys/kern/tty.c:1137) ->
	 *    callout_pending() != 0 -> ng_uncallout() -> item+ref LEAKED.
	 *    (Same ngt_close code path as fd-close; closing the master
	 *    first avoids the tty output-drain wait.) */
	{
		int zero = 0;
		close(master);
		if (ioctl(slave, TIOCSETD, &zero) < 0) {
			perror("TIOCSETD 0"); return (-1);
		}
		close(slave);
	}
	close(ds);
	close(cs);
	return (0);
}

int
main(int argc, char **argv)
{
	int rounds, i, drain = 0;

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

	for (i = 0; i < rounds; i++) {
		if (cycle(drain, i) < 0)
			exit(1);
		if ((i % 500) == 499) {
			printf("... %d rounds (%s)\n", i + 1,
			    drain ? "drain/control" : "stall/leak");
			fflush(stdout);
		}
	}
	printf("done: %d rounds (mode=%s)\n", rounds,
	    drain ? "drain/control" : "stall/leak");
	return (0);
}
