/*
 * harness.c - Deterministic userspace replication of the unvalidated resident
 *             attribute data-offset OOB read in ntfs_attrtontvattr()
 *             (sys/vfs/ntfs/ntfs_subr.c:557-564).
 *
 * ROOT CAUSE (confirmed by source trace of sys/vfs/ntfs/ntfs_subr.c):
 *   554:   } else {                                      // resident attribute
 *   555:       vap->va_compressalg = 0;
 *   557:       vap->va_datalen = rap->a_r.a_datalen;     // u16 from disk, NO bound
 *   558:       vap->va_allocated = rap->a_r.a_datalen;
 *   561:       vap->va_datap = kmalloc(vap->va_datalen, M_NTFSRDATA, M_WAITOK);
 *   563:       memcpy(vap->va_datap,
 *   564:              (caddr_t) rap + rap->a_r.a_dataoff, // <-- a_dataoff u16 UNCHECKED
 *                      rap->a_r.a_datalen);              // <-- a_datalen u16 UNCHECKED
 *
 *   rap is a struct attr * pointing into mfrp, the MFT record buffer:
 *     mfrp = kmalloc(ntfs_bntob(ntmp->ntm_bpmftrec), M_TEMP, M_WAITOK);  // line 263
 *   typically 1024 or 4096 bytes.  The on-disk u16 a_dataoff/a_datalen are
 *   trusted without any bounds check, so a crafted image with a_dataoff past
 *   the record boundary makes memcpy read past mfrp into adjacent M_TEMP slab
 *   heap.  The leaked bytes land in vap->va_datap and are later exposed to
 *   userspace via ntfs_readntvattr_plain():1594  uiomove(vap->va_datap+roff,
 *   rsize, uio).
 *
 * WHY A HARNESS
 * ------------
 * The live trigger (mount_ntfs on a crafted image) reaches the vulnerable
 * memcpy at mount time during ntfs_loadntnode(), but whether the OOB read
 * manifests as a panic or a silent leak depends on what byte pattern lives
 * in the adjacent slab chunk (determined by prior kernel allocations).  This
 * harness makes the OOB read DETERMINISTIC by placing a guard page
 * (PROT_NONE) immediately after the record buffer, so any read past the
 * buffer end faults (SIGSEGV) — proving the primitive exists regardless of
 * slab layout.  It also demonstrates that the proposed fix (bounding
 * a_dataoff+a_datalen against the record size) eliminates the OOB read.
 *
 * Build:  cc -O2 -o harness harness.c
 * Run:    ./harness {clean|oob_dataoff} [apply_fix]
 * Exit:   0 = memcpy completed within bounds (clean, or fix rejected bad input)
 *         2 = SIGSEGV caught -> OOB read past record (proof of the bug)
 */

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <signal.h>
#include <setjmp.h>
#include <sys/mman.h>

/*
 * Mirror the exact struct layouts from sys/vfs/ntfs/ntfs.h.
 *
 *   struct attrhdr {                  // 16 bytes
 *       u_int32_t a_type;
 *       u_int32_t reclen;
 *       u_int8_t  a_flag;
 *       u_int8_t  a_namelen;
 *       u_int8_t  a_nameoff;
 *       u_int8_t  reserved1;
 *       u_int8_t  a_compression;
 *       u_int8_t  reserved2;
 *       u_int16_t a_index;
 *   };
 *   struct attr {
 *       struct attrhdr a_hdr;
 *       union {
 *           struct {                  // resident (a_S_r)
 *               u_int16_t a_datalen;
 *               u_int16_t reserved1;
 *               u_int16_t a_dataoff;
 *               u_int16_t a_indexed;
 *           } a_S_r;
 *           ...
 *       } a_S;
 *   };
 *   #define a_r   a_S.a_S_r
 */
struct attrhdr {
    uint32_t a_type;
    uint32_t reclen;
    uint8_t  a_flag;
    uint8_t  a_namelen;
    uint8_t  a_nameoff;
    uint8_t  reserved1;
    uint8_t  a_compression;
    uint8_t  reserved2;
    uint16_t a_index;
} __attribute__((packed));

struct attr_resident {
    struct attrhdr hdr;
    uint16_t a_datalen;       /* offset 16 */
    uint16_t reserved1;       /* offset 18 */
    uint16_t a_dataoff;       /* offset 20 -- THE BUG: unchecked */
    uint16_t a_indexed;       /* offset 22 */
} __attribute__((packed));

#define REC_BYTES  1024       /* classic MFT record size; also test 4096 */
#define REC_BYTES_BIG 4096    /* matches the sibling image geometry */
#define NTFS_AF_INRUN 0x01

/* ---- signal handling so we can catch the OOB SIGSEGV cleanly ---- */
static sigjmp_buf jb;
static volatile int got_sig;
static void segv_handler(int sig) { (void)sig; got_sig = SIGSEGV; siglongjmp(jb, 1); }

/*
 * Mirror of ntfs_attrtontvattr() resident path, lines 554-565.
 *   rap        = pointer to the attribute inside the MFT record buffer
 *   rec_size   = size of the MFT record buffer (mfrp allocation)
 *   rec_base   = start of the MFT record buffer (for bounds computation)
 *
 * Returns: 0 = memcpy within bounds; 1 = OOB detected by fix (rejected);
 *          exits via SIGSEGV if the buggy path reads past the guard page.
 */
static int resident_memcpy_buggy(struct attr_resident *rap,
                                 uint8_t *rec_base, int rec_size,
                                 uint8_t *out, int out_sz)
{
    uint16_t datalen = rap->a_datalen;     /* line 557 */
    uint16_t dataoff = rap->a_dataoff;     /* unchecked */
    /* line 561: kmalloc(datalen) -> out (caller-provided) */
    int n = (datalen < out_sz) ? datalen : out_sz;
    /* line 563-564: memcpy(out, rap + dataoff, datalen) -- THE BUG */
    memcpy(out, (uint8_t *)rap + dataoff, n);
    return 0;
}

/*
 * The proposed fix: validate that (rap + a_dataoff + a_datalen) does not
 * exceed the MFT record buffer boundary before the memcpy.
 */
static int resident_memcpy_fixed(struct attr_resident *rap,
                                 uint8_t *rec_base, int rec_size,
                                 uint8_t *out, int out_sz)
{
    uint16_t datalen = rap->a_datalen;
    uint16_t dataoff = rap->a_dataoff;
    uintptr_t rap_addr = (uintptr_t)rap;
    uintptr_t rec_end  = (uintptr_t)rec_base + rec_size;

    /* NEW: validate a_dataoff+a_datalen fits within the record buffer */
    if (rap_addr + dataoff + datalen > rec_end)
        return 1;   /* EINVAL: reject the malformed attribute */

    int n = (datalen < out_sz) ? datalen : out_sz;
    memcpy(out, (uint8_t *)rap + dataoff, n);
    return 0;
}

/*
 * Build a synthetic resident attribute inside the record buffer.
 *   mode "clean":       a_dataoff points to valid data within the record.
 *   mode "oob_dataoff": a_dataoff pushed past the record end so memcpy
 *                       reads into the guard page.
 */
static struct attr_resident *
fill_record(uint8_t *rec, int rec_size, const char *mode, int attr_off)
{
    memset(rec, 0xA5, rec_size);   /* fill with sentinel (non-zero residue) */

    struct attr_resident *rap = (struct attr_resident *)(rec + attr_off);
    rap->hdr.a_type    = 0x80;           /* NTFS_A_DATA */
    rap->hdr.a_flag    = 0;              /* resident */
    rap->hdr.reclen    = 64;

    if (strcmp(mode, "clean") == 0) {
        /* legitimate resident attribute: data right after the header */
        rap->a_datalen = 32;             /* 32 bytes of data */
        rap->a_dataoff = 24;             /* data starts at hdr+24 (offset 24) */
        /* write some known data at rap+24 */
        memset((uint8_t *)rap + 24, 'D', 32);
    } else if (strcmp(mode, "oob_dataoff") == 0) {
        /* BUG: a_dataoff pushed past the record end.
         * rap is at attr_off within the record; set dataoff so that
         * rap + dataoff + datalen exceeds rec_base + rec_size. */
        rap->a_datalen = 64;
        /* push the read target past the buffer end:
         *   rap_addr + dataoff + 64 > rec_base + rec_size
         *   dataoff > rec_size - attr_off - 64                         */
        rap->a_dataoff = (uint16_t)(rec_size - attr_off);  /* start AT the end */
    } else {
        fprintf(stderr, "unknown mode '%s'\n", mode);
        exit(3);
    }
    return rap;
}

int main(int argc, char **argv)
{
    if (argc < 2) {
        fprintf(stderr,
            "usage: %s {clean|oob_dataoff} [apply_fix] [recsize]\n", argv[0]);
        fprintf(stderr, "  recsize: 1024 (default) or 4096\n");
        return 3;
    }
    const char *mode = argv[1];
    int apply_fix = (argc >= 3 && strcmp(argv[2], "apply_fix") == 0);
    int rec_size = (argc >= 4) ? atoi(argv[3]) : REC_BYTES;
    if (rec_size != REC_BYTES && rec_size != REC_BYTES_BIG) {
        fprintf(stderr, "recsize must be 1024 or 4096\n");
        return 3;
    }

    long pagesz = 4096;
    size_t mapsz = ((size_t)rec_size + pagesz - 1) & ~((size_t)pagesz - 1);
    /* Allocate rec_size writable bytes + one guard page right after. */
    uint8_t *base = mmap(NULL, mapsz + pagesz, PROT_READ | PROT_WRITE,
                         MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (base == MAP_FAILED) { perror("mmap"); return 3; }
    if (mprotect(base + mapsz, pagesz, PROT_NONE) != 0) {
        perror("mprotect"); return 3; }
    /* Place rec at the END of the writable region so the guard page is
     * immediately after byte rec_size-1 (truest mirror of a slab chunk). */
    uint8_t *rec = base + mapsz - rec_size;

    /* attribute starts at offset 56 within the record (after a typical
     * FILE header + fixup array), mirroring fr_attroff. */
    int attr_off = 56;
    struct attr_resident *rap = fill_record(rec, rec_size, mode, attr_off);

    /* install SIGSEGV handler */
    struct sigaction sa, oldsa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = segv_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGSEGV, &sa, &oldsa);

    uint8_t out[256];
    memset(out, 0, sizeof(out));
    got_sig = 0;
    int rc;
    if (sigsetjmp(jb, 1) == 0) {
        if (apply_fix)
            rc = resident_memcpy_fixed(rap, rec, rec_size, out, sizeof(out));
        else
            rc = resident_memcpy_buggy(rap, rec, rec_size, out, sizeof(out));
    } else {
        rc = 2;   /* SIGSEGV -> OOB read past the guard page */
    }
    sigaction(SIGSEGV, &oldsa, NULL);

    const char *verdict;
    switch (rc) {
        case 0:
            verdict = "memcpy within bounds (clean)";
            break;
        case 1:
            verdict = "FIX REJECTED malformed attribute (EINVAL) - OOB prevented";
            break;
        case 2:
            verdict = "SIGSEGV -> OOB READ past record into guard page "
                      "(= adjacent slab heap in kernel): LEAK CONFIRMED";
            break;
        default:
            verdict = "unknown";
            break;
    }

    printf("mode=%-12s apply_fix=%-3d recsz=%-4d attr_off=%d "
           "a_dataoff=0x%04x a_datalen=%-3u  -> rc=%d  %s\n",
           mode, apply_fix, rec_size, attr_off, rap->a_dataoff, rap->a_datalen,
           rc, verdict);

    /* For the clean case, show the copied data to prove it works normally. */
    if (rc == 0 && strcmp(mode, "clean") == 0) {
        printf("  copied %u bytes: first 8 = ", rap->a_datalen);
        for (int i = 0; i < 8 && i < (int)rap->a_datalen; i++)
            printf("%02x ", out[i]);
        printf("(expected 44 44 ... = 'D' sentinel)\n");
    }
    return (rc == 0 || rc == 1) ? 0 : rc;
}
