/*
 * DF-3521 — ng_pptpgre deterministic timer-accounting leak (root-local).
 *
 * MECHANISM (all citations against the audited tree):
 *
 *  ng_pptpgre_start_recv_ack_timer()  ng_pptpgre.c:762-775
 *      a->rackTimerPtr = kmalloc(sizeof(node_p));   (:762)
 *      node->refs++;                                (:768)  <- ref owned by timer
 *      priv->timers++;                              (:769)
 *      callout_reset(&a->rackTimer, ticks, ..., a->rackTimerPtr); (:773)
 *
 *  ng_pptpgre_stop_recv_ack_timer()   ng_pptpgre.c:781-792
 *      if (callout_stop(&a->rackTimer)) {           (:786)
 *          kfree(a->rackTimerPtr);                  (:787)
 *          priv->timers--;                          (:788)
 *          ng_unref(node);                          (:789)
 *      }
 *      a->rackTimerPtr = NULL;                      (:791)
 *
 * The whole cleanup depends on callout_stop() returning non-zero for a
 * callout that was armed and got cancelled before firing.  On this
 * kernel that contract is BROKEN (DF-3515):
 *
 *   sys/kern/kern_timeout.c
 *     _callout_cancel_or_stop():
 *       :879-881  clear PREVENTED, set STOP, _callout_update_spinlocked(c)
 *       :226-229 / :266-277  the inline update REMOVES the callout from
 *                            the wheel, clears CALLOUT_SET, sets PREVENTED
 *       :888-891  early exit: SET is now clear, so
 *                 `if (sync == 0 || (flags & (INPROG|SET)) == 0) return 0;`
 *                 fires and returns 0 — the PREVENTED-reporting path at
 *                 :895-900 is never reached.
 *
 * So for an ARMED, not-yet-fired rack/sack timer, callout_stop() returns 0
 * even though it DID cancel it and the callback will NEVER run.  ng_pptpgre
 * then skips the kfree/timers--/ng_unref trio and only NULLs the pointer:
 *
 *   => the 8-byte rackTimerPtr allocation leaks   (M_NETGRAPH)
 *   => the node reference taken at :768 leaks    (node never freed)
 *   => priv->timers stays incremented FOREVER
 *   => ng_pptpgre_rmnode() :437  `if (priv->timers == 0)` never true
 *      => priv (~208 bytes) leaks on every node shutdown that had a
 *         timer armed; the NG_INVALID-branch free in the callbacks
 *         (:813-816, :917-920) can't happen either — the callback was
 *         cancelled.
 *
 * TRIGGER: one data frame through "upper" arms the rack timer
 * (ng_pptpgre_xmit :558-559), then NGM_PPTPGRE_SET_CONFIG arrives ->
 * ng_pptpgre_reset (:977-978) stops the armed timer -> leak.  That is the
 * NORMAL PPTP configuration path; under a live session every
 * ack-arrival (:679-681 stop/restart churn) leaks the same way.
 *
 * Graph:
 *   ctlA:p0  <-->  pptp:upper     (MKPEER on ctlA)
 *   ctlB:p1  <-->  pptp:lower     (CONNECT on ctlB, peer path "pptp:")
 *   dsockA connect "ctlA:"        (inject frames to hook "p0")
 *   dsockB connect "ctlB:"        (drain GRE frames from hook "p1")
 *
 * Build:  cc -O2 -lpthread -o leak_test leak_test.c
 * Run:    ./leak_test <n_cycles> [0|1 shutdown-per-cycle]
 * Success criterion: M_NETGRAPH in-use bytes grows ~monotonically by
 * hundreds of KB for tens of thousands of cycles on the STOCK kernel,
 * and stays ~flat on a kernel with the kern_timeout.c contract fix.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/types.h>

#include <netgraph/ng_message.h>
#include <netgraph/socket/ng_socket.h>
#include <netgraph/pptpgre/ng_pptpgre.h>

#ifndef AF_NETGRAPH
#define AF_NETGRAPH 32
#endif

#define NGM_PPTPGRE_COOKIE_    NGM_PPTPGRE_COOKIE
#define NGM_PPTPGRE_SET_CONFIG_ NGM_PPTPGRE_SET_CONFIG
#define pptp_conf               ng_pptpgre_conf

struct my_sockaddr_ng { unsigned char sa_len; unsigned char sa_family; char sg_data[256]; };

static int csockA, csockB, dsockA, dsockB;

static int
ng_send_ctrl_path(int csock, u_int32_t cookie, u_int32_t cmd,
    const void *data, u_int16_t arglen, const char *path)
{
	size_t total = sizeof(struct ng_mesg) + arglen;
	char *buf = calloc(1, total);
	if (!buf) return -1;
	struct ng_mesg *m = (struct ng_mesg *)buf;
	m->header.version    = NG_VERSION;
	m->header.arglen     = arglen;
	m->header.flags      = 0;
	m->header.token      = 0;
	m->header.typecookie = cookie;
	m->header.cmd        = cmd;
	if (data && arglen)
		memcpy(m->data, data, arglen);

	struct my_sockaddr_ng dst;
	memset(&dst, 0, sizeof(dst));
	dst.sa_len    = 2 + strlen(path) + 1;
	dst.sa_family = AF_NETGRAPH;
	strlcpy(dst.sg_data, path, sizeof(dst.sg_data));

	ssize_t rc = sendto(csock, buf, total, 0, (struct sockaddr *)&dst, dst.sa_len);
	int e = errno;
	free(buf);
	if (rc < 0) {
		fprintf(stderr, "ctrl sendto cookie=%u cmd=%u path=%s: %s (errno=%d)\n",
		    cookie, cmd, path, strerror(e), e);
		return -1;
	}
	return 0;
}

static void
ng_mkhookaddr(struct my_sockaddr_ng *sa, const char *hook)
{
	memset(sa, 0, sizeof(*sa));
	sa->sa_len = 2 + strlen(hook) + 1;
	sa->sa_family = AF_NETGRAPH;
	strlcpy(sa->sg_data, hook, sizeof(sa->sg_data));
}

static int
bind_ng_name(int sock, const char *name)
{
	struct my_sockaddr_ng sa;
	memset(&sa, 0, sizeof(sa));
	sa.sa_len = 2 + strlen(name) + 1;
	sa.sa_family = AF_NETGRAPH;
	strlcpy(sa.sg_data, name, sizeof(sa.sg_data));
	return bind(sock, (struct sockaddr *)&sa, sa.sa_len);
}

static int
connect_ng_path(int sock, const char *path)
{
	struct my_sockaddr_ng sa;
	memset(&sa, 0, sizeof(sa));
	sa.sa_len = 2 + strlen(path) + 1;
	sa.sa_family = AF_NETGRAPH;
	strlcpy(sa.sg_data, path, sizeof(sa.sg_data));
	return connect(sock, (struct sockaddr *)&sa, sa.sa_len);
}

/* Build: ctlA:p0<->pptp:upper , ctlB:p1<->pptp:lower ; returns 0 on ok */
static int
build_graph(void)
{
	struct ngm_mkpeer mkpeer;
	struct ngm_connect conn;
	struct ngm_name namebuf;

	memset(&mkpeer, 0, sizeof(mkpeer));
	strlcpy(mkpeer.type, "pptpgre", sizeof(mkpeer.type));
	strlcpy(mkpeer.ourhook, "p0", sizeof(mkpeer.ourhook));
	strlcpy(mkpeer.peerhook, "upper", sizeof(mkpeer.peerhook));
	if (ng_send_ctrl_path(csockA, NGM_GENERIC_COOKIE, NGM_MKPEER,
	    &mkpeer, sizeof(mkpeer), ".") < 0)
		return -1;

	memset(&namebuf, 0, sizeof(namebuf));
	strlcpy(namebuf.name, "pptp", sizeof(namebuf.name));
	if (ng_send_ctrl_path(csockA, NGM_GENERIC_COOKIE, NGM_NAME,
	    &namebuf, sizeof(namebuf), "p0") < 0)
		return -1;

	memset(&conn, 0, sizeof(conn));
	strlcpy(conn.path, "pptp:", sizeof(conn.path));
	strlcpy(conn.ourhook, "p1", sizeof(conn.ourhook));
	strlcpy(conn.peerhook, "lower", sizeof(conn.peerhook));
	if (ng_send_ctrl_path(csockB, NGM_GENERIC_COOKIE, NGM_CONNECT,
	    &conn, sizeof(conn), ".") < 0)
		return -1;
	return 0;
}

static int
set_conf(int enabled)
{
	struct ng_pptpgre_conf c;
	memset(&c, 0, sizeof(c));
	c.enabled = enabled;
	c.enableDelayedAck = 0;
	c.enableAlwaysAck = 0;
	c.cid = 1;
	c.peerCid = 1;
	c.recvWin = 16;
	c.peerPpd = 0;
	return ng_send_ctrl_path(csockA, NGM_PPTPGRE_COOKIE_,
	    NGM_PPTPGRE_SET_CONFIG_, &c, sizeof(c), "pptp:");
}

static int
send_one_frame(void)
{
	unsigned char frame[64];
	memset(frame, 0, sizeof(frame));
	frame[0] = 0xff; frame[1] = 0x03;   /* PPP address/control-ish payload */
	struct my_sockaddr_ng dst;
	ng_mkhookaddr(&dst, "p0");
	ssize_t rc = sendto(dsockA, frame, sizeof(frame), 0,
	    (struct sockaddr *)&dst, dst.sa_len);
	return (rc < 0) ? -1 : 0;
}

static void
drain_some(void)
{
	unsigned char buf[2048];
	struct my_sockaddr_ng from;
	socklen_t fl;
	int n = 0;
	while (n++ < 64) {
		fl = sizeof(from);
		ssize_t rc = recvfrom(dsockB, buf, sizeof(buf), MSG_DONTWAIT,
		    (struct sockaddr *)&from, &fl);
		if (rc < 0)
			break;
	}
}

static void
shutdown_node(void)
{
	ng_send_ctrl_path(csockA, NGM_GENERIC_COOKIE, NGM_SHUTDOWN,
	    NULL, 0, "pptp:");
}

int
main(int argc, char **argv)
{
	long cycles = (argc > 1) ? atol(argv[1]) : 20000;
	int do_shutdown = (argc > 2) ? atoi(argv[2]) : 0;
	long i, ok = 0, fail = 0;

	if (geteuid() != 0) {
		fprintf(stderr, "must be root (ng control socket is gated by "
		    "caps_priv_check(SYSCAP_RESTRICTEDROOT) "
		    "sys/netgraph/socket/ng_socket.c:172)\n");
		return 2;
	}

	csockA = socket(AF_NETGRAPH, SOCK_DGRAM, NG_CONTROL);
	csockB = socket(AF_NETGRAPH, SOCK_DGRAM, NG_CONTROL);
	if (csockA < 0 || csockB < 0) {
		fprintf(stderr, "socket(ctl): %s\n", strerror(errno));
		return 2;
	}
	if (bind_ng_name(csockA, "ctlA") < 0 || bind_ng_name(csockB, "ctlB") < 0) {
		fprintf(stderr, "bind(ctl): %s\n", strerror(errno));
		return 2;
	}

	dsockA = socket(AF_NETGRAPH, SOCK_DGRAM, NG_DATA);
	dsockB = socket(AF_NETGRAPH, SOCK_DGRAM, NG_DATA);
	if (dsockA < 0 || dsockB < 0) {
		fprintf(stderr, "socket(data): %s\n", strerror(errno));
		return 2;
	}
	{
		int rb = 1 << 20;
		(void)setsockopt(dsockB, SOL_SOCKET, SO_RCVBUF, &rb, sizeof(rb));
	}
	if (connect_ng_path(dsockA, "ctlA:") < 0 ||
	    connect_ng_path(dsockB, "ctlB:") < 0) {
		fprintf(stderr, "connect(data): %s\n", strerror(errno));
		return 2;
	}

	for (i = 0; i < cycles; i++) {
		if (do_shutdown) {
			/* fresh node each cycle: build, enable, 1 frame, reset, kill */
			if (build_graph() < 0) { fail++; continue; }
			if (set_conf(1) < 0) { fail++; continue; }
			if (send_one_frame() < 0) { fail++; continue; }
			drain_some();
			if (set_conf(1) < 0) { fail++; continue; }  /* reset -> stop armed rack timer */
			shutdown_node();                            /* timers!=0 -> priv leak */
		} else {
			/* single long-lived node: arm + cancel rack timer each cycle */
			if (i == 0) {
				if (build_graph() < 0 || set_conf(1) < 0) { fail++; continue; }
			}
			if (send_one_frame() < 0) { fail++; continue; }
			drain_some();
			if (set_conf(1) < 0) { fail++; continue; }  /* reset stops armed timer */
		}
		ok++;
		if ((i % 5000) == 4999) {
			fprintf(stderr, "cycle %ld/%ld (ok=%ld fail=%ld)\n",
			    i + 1, cycles, ok, fail);
			fflush(stderr);
		}
	}
	if (!do_shutdown)
		shutdown_node();

	printf("DONE cycles=%ld ok=%ld fail=%ld (now check: vmstat -m | grep -i netgraph)\n",
	    cycles, ok, fail);
	return 0;
}
