/*
 * DF-0784 deterministic harness.
 *
 * Transcribes the exact arithmetic of ext2_readlink()
 * (sys/vfs/ext2fs/ext2_vnops.c:1345-1357) line-for-line, using a poisoned
 * userland "struct inode" so the production unbounded-heap-read primitive is
 * demonstrated deterministically, independent of the live slab layout (which
 * on a GENERIC kernel manifests as a panic once copyout walks past the slab —
 * see panic.txt; on a production/INVARIANTS-OFF kernel the disclosure is
 * silent up to the first unmapped page).
 *
 * Build:  cc -O2 -o harness harness.c
 * Run:    ./harness [bufkb]    (default 4 KB)
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stddef.h>

/* ---- minimal mirror of the relevant kernel types ------------------------- */

typedef uint64_t u_quad_t;
typedef uint64_t uint64_t;
typedef uint32_t uint32_t;
typedef int32_t  int32_t;
typedef int      boolean_t;

#define EXT2_NDADDR  12
#define EXT2_NIADDR  3
#define EXT2_MAXSYMLINKLEN  ((EXT2_NDADDR + EXT2_NIADDR) * sizeof(uint32_t)) /* 60 */

/* Mirror of `struct inode` — only the fields touched by ext2_readlink + a few
 * neighbours to represent what the over-read walks into.  We deliberately
 * make this much larger than i_shortlink so the leak has somewhere to read. */
struct inode {
    char      padding_before[64];      /* mimic preceding struct fields */
    uint64_t  i_size;                  /* file byte count (uint64!) */
    uint64_t  i_blocks;
    uint32_t  i_db[EXT2_NDADDR];       /* i_shortlink alias */
    uint32_t  i_ib[EXT2_NIADDR];
    /* After this point in real life is the next slab object / heap residue.
     * We poison it so the leak content is unambiguous. */
    unsigned char heap_residue[4096 - 64 - 8 - 8 - 48 - 12];
};

/* Mirror of `struct uio` (only what uiomove reads). */
struct iovec { void *iov_base; size_t iov_len; };
struct uio {
    struct iovec *uio_iov;
    int           uio_iovcnt;
    size_t        uio_resid;
    /* ... */
};

/* ---- exact transcription of kern_subr.c uiomove() loop -------------------- */
static int
uiomove_emul(unsigned char *cp, size_t n, struct uio *uio)
{
    struct iovec *iov;
    size_t cnt, tot = 0;
    int error = 0;

    while (n > 0 && uio->uio_resid) {
        iov = uio->uio_iov;
        cnt = iov->iov_len;
        if (cnt == 0) { uio->uio_iov++; uio->uio_iovcnt--; continue; }
        if (cnt > n)  cnt = n;
        tot += cnt;
        /* UIO_USERSPACE + UIO_READ -> copyout */
        memcpy(iov->iov_base, cp, cnt);
        iov->iov_base = (char *)iov->iov_base + cnt;
        iov->iov_len -= cnt;
        uio->uio_resid -= cnt;
        cp += cnt;
    }
    return error;
}

/* ---- exact transcription of ext2_vnops.c:1345-1357 ----------------------- */
static int
ext2_readlink_emul(struct inode *ip, struct uio *uio, int mnt_maxsymlinklen)
{
    int isize;                                  /* line 1350 */
    isize = ip->i_size;                         /* line 1352: TRUNCATES uint64 -> int */
    if (isize < mnt_maxsymlinklen) {            /* line 1353: signed compare */
        uiomove_emul((unsigned char *)ip->i_db, /* line 1354: i_shortlink = i_db */
                     (size_t)isize,             /* int -> size_t (SIGN EXTEND!) */
                     uio);
        return (0);
    }
    return (1);                                 /* VOP_READ path (untaken) */
}

int main(int argc, char **argv)
{
    size_t bufsz = (argc >= 2) ? (size_t)strtoul(argv[1], NULL, 0) * 1024 : 4096;
    if (bufsz == 0) bufsz = 4096;

    /* Build a poisoned "inode" slab. */
    struct inode *ip = calloc(1, sizeof(*ip));
    /* Write the legit short symlink target into i_shortlink (= i_db). */
    const char *target = "target_foobar_link";   /* 18 bytes */
    memcpy(ip->i_db, target, strlen(target));
    /* Set the corrupted on-disk-equivalent i_size: bit 31 set. */
    ip->i_size = 0x0000000080000000ULL;

    /* Poison everything after the legit target so we can SEE the leak. */
    for (size_t i = strlen(target); i < sizeof(ip->heap_residue) + sizeof(ip->i_ib); i++) {
        unsigned char *p = (unsigned char *)ip->i_db + i;
        *p = (unsigned char)(0xA0 + (i & 0x3F));
    }

    printf("=== DF-0784 harness: ext2_readlink arithmetic transcription ===\n");
    printf("ip->i_size (uint64)   = 0x%016llx\n", (unsigned long long)ip->i_size);
    int isize_buggy = (int)ip->i_size;
    printf("isize (int, truncated)= %d  (0x%08x)\n", isize_buggy, isize_buggy);
    printf("isize (int -> size_t) = 0x%016llx  (sign-extended)\n",
           (unsigned long long)(size_t)isize_buggy);
    printf("EXT2_MAXSYMLINKLEN    = %d\n", EXT2_MAXSYMLINKLEN);
    printf("isize < MAXSYMLINKLEN ? %s  (TRUE => fast path TAKEN)\n",
           (isize_buggy < (int)EXT2_MAXSYMLINKLEN) ? "YES" : "no");
    printf("user buffer size      = %zu bytes\n", bufsz);

    unsigned char *userbuf = malloc(bufsz);
    struct iovec iv = { .iov_base = userbuf, .iov_len = bufsz };
    struct uio uio = { .uio_iov = &iv, .uio_iovcnt = 1, .uio_resid = bufsz };

    int rc = ext2_readlink_emul(ip, &uio, EXT2_MAXSYMLINKLEN);

    size_t copied = bufsz - uio.uio_resid;
    printf("readlink returned     = %zu bytes  (rc=%d)\n", copied, rc);
    printf("legit target length   = %zu bytes\n", strlen(target));
    if (copied > strlen(target)) {
        size_t leaked = copied - strlen(target);
        printf("[!!] LEAKED %zu bytes of fake-kernel-heap past the legit target\n",
               leaked);
    }
    /* Show first 64 bytes of what landed in the user buffer. */
    printf("\nfirst 64 bytes of user buffer (target string then poison):\n");
    for (size_t i = 0; i < 64 && i < copied; i++) {
        printf("%02x ", userbuf[i]);
        if ((i & 15) == 15) printf("\n");
    }
    printf("\nbytes 32-64 from offset 2000 (deep into the over-read region):\n");
    for (size_t i = 2000; i < 2032 && i < copied; i++) {
        printf("%02x ", userbuf[i]);
        if (((i - 2000) & 15) == 15) printf("\n");
    }
    printf("\nlast 32 bytes of user buffer:\n");
    for (size_t i = (copied > 32 ? copied - 32 : 0); i < copied; i++) {
        printf("%02x ", userbuf[i]);
        if (((i - (copied > 32 ? copied - 32 : 0)) & 15) == 15) printf("\n");
    }
    printf("\n\nConclusion: a %d-byte int truncation of a uint64 i_size yields a\n"
           "size_t count of 0x%016llx; uiomove copies that many bytes from\n"
           "i_shortlink into user RAM, bounded only by the user buffer / the\n"
           "first unmapped kernel page.  In the live kernel (see panic.txt) the\n"
           "latter ends in a vm_fault panic from ext2_readlink+0x48.\n",
           isize_buggy, (unsigned long long)(size_t)isize_buggy);
    free(userbuf);
    free(ip);
    return 0;
}
