/*
 * harness.c - Deterministic userspace proof of the DF-0791 unvalidated
 * subnode-dive OOB read in ntfs_ntlookupfile()
 * (sys/vfs/ntfs/ntfs_subr.c:1006-1011).
 *
 * Mirrors the EXACT kernel walk + dive logic:
 *
 *   aoff = sizeof(struct attr_indexroot);          // 32
 *   do {
 *     iep = (struct attr_indexentry *)(rdbuf + aoff);
 *     for (; !(iep->ie_flag & LAST) && (rdsize > aoff);
 *          aoff += iep->reclen,
 *          iep = (struct attr_indexentry *)(rdbuf + aoff))
 *     {
 *       res = name_cmp(iep->fname, lookup);     // entry "zzzzzz" > lookup "a"
 *       if (res > 0) break;
 *       ...
 *     }
 *     if (iep->ie_flag & SUBNODE) {
 *       cn = *(cn_t *)(rdbuf + aoff + iep->reclen - sizeof(cn_t));  // <-- BUG
 *       ...
 *     }
 *   } while (1);
 *
 * The buffer is placed at the END of a writable page immediately before a
 * PROT_NONE guard page. Any read past rdsize/the allocation faults
 * deterministically (SIGSEGV), proving the primitive regardless of kernel
 * slab layout.
 *
 * Modes:
 *   clean      reclen sane, dive read in-bounds      -> rc=0  (cn read OK)
 *   oob        reclen=0xFFFF, dive read past buffer  -> rc=2  SIGSEGV (OOB read)
 *   tiny       reclen=2 (< sizeof(cn_t))             -> rc=2  SIGSEGV (OOB read)
 *
 * With apply_fix=1, the proposed guard (reclen>=8 && aoff+reclen<=rdsize) runs
 * and rejects the malformed entry cleanly -> rc=-1 (EINVAL) instead of faulting.
 *
 * Build: cc -O2 -o harness harness.c
 * Run:   ./harness [clean|oob|tiny] [0|1]
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <signal.h>
#include <setjmp.h>
#include <sys/mman.h>
#include <unistd.h>

typedef uint64_t cn_t;
typedef uint32_t u32;
typedef uint16_t u16;

#define SUBNODE  0x01u
#define LAST     0x02u

#define INDEXROOT_HDR   32      /* sizeof(struct attr_indexroot) */
#define IEP_FIXED       82      /* fixed part of attr_indexentry up to ie_fname */
#define CN_SIZE         sizeof(cn_t)   /* 8 */

/* minimal on-disk index entry shape we care about */
struct iep_disk {
    u32 number; u32 unk1; u16 reclen; u16 size; u32 flag;
    u32 fpnumber; u32 unk2;
    char times[32];
    char rest[8];
    /* ie_fnamelen @ +80, ie_fnametype @ +81, ie_fname @ +82 */
};

static sigjmp_buf jb;
static volatile int faulted;

static void handler(int s, siginfo_t *si, void *uc) {
    (void)s; (void)si; (void)uc;
    faulted = 1;
    siglongjmp(jb, 1);
}

/* Build a buffer of `cap` usable bytes backed by pages, with a PROT_NONE
 * guard page immediately after the usable region. Returns the usable base
 * via *out and the raw mmap base via *base (for munmap). */
static void make_guard_buf(unsigned cap, unsigned char **out, unsigned char **base,
                           unsigned *maplen) {
    long ps = sysconf(_SC_PAGESIZE);
    unsigned pages = (cap + ps - 1) / ps;
    unsigned len = (pages + 1) * ps;            /* +1 guard page */
    void *m = mmap(NULL, len, PROT_READ|PROT_WRITE,
                   MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
    if (m == MAP_FAILED) { perror("mmap"); exit(3); }
    /* guard the LAST page */
    if (mprotect((char*)m + pages*ps, ps, PROT_NONE) != 0) {
        perror("mprotect"); exit(3);
    }
    /* usable region = first `cap` bytes; the guard sits right after byte cap-1
     * only if cap is page-aligned; otherwise there is padding before the guard.
     * To make the OOB fault as early as possible we place the data at the END
     * of the last usable page so any overshoot hits the guard immediately. */
    unsigned region_pages = pages * ps;
    unsigned char *usable = (unsigned char*)m + (region_pages - cap);
    memset(usable, 0xAA, cap);                  /* fill with residue marker */
    *out = usable;
    *base = m;
    *maplen = len;
}

/* Replicate the kernel dive read. Returns:
 *    0  -> cn read in bounds (no OOB)
 *   -1  -> fix rejected malformed entry (EINVAL)
 * If the read faults, the signal handler longjmps and we report rc=2. */
static int do_dive(unsigned char *rdbuf, u32 rdsize, u32 aoff, u16 reclen,
                   int apply_fix, cn_t *cn_out) {
    if (apply_fix) {
        /* proposed fix: entry (incl trailing 8B VCN) must fit in valid data */
        if (reclen < CN_SIZE)            return -1;
        if (aoff > rdsize)               return -1;
        if (reclen > (u16)(rdsize - aoff)) return -1;
        /* also guard the cn read itself */
        uint64_t end = (uint64_t)aoff + (uint64_t)reclen;
        if (end > rdsize)                return -1;
    }
    /* the BUGGY kernel line: cn = *(cn_t*)(rdbuf + aoff + reclen - 8); */
    cn_t cn = *(cn_t *)(rdbuf + aoff + reclen - CN_SIZE);
    if (cn_out) *cn_out = cn;
    return 0;
}

int main(int argc, char **argv) {
    const char *mode = (argc > 1) ? argv[1] : "oob";
    int apply_fix = (argc > 2) ? atoi(argv[2]) : 0;

    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_sigaction = handler;
    sa.sa_flags = SA_SIGINFO | SA_NODEFER;
    sigaction(SIGSEGV, &sa, NULL);
    sigaction(SIGBUS,  &sa, NULL);

    /* layout: INDEX_ROOT header (32) + one index entry "zzzzzz".
     * Real entry footprint ~ 88 bytes (IEP_FIXED + name). The dive consults
     * the entry's reclen field, which we set per mode. rdsize = header + real
     * entry bytes (so the walk ENTERS the entry), but the dive read goes to
     * aoff+reclen-8 which for malformed reclen is past rdsize -> guard fault. */
    unsigned entry_real = (IEP_FIXED + 12 + 7) & ~7u;   /* 96 bytes */
    unsigned rdsize = INDEXROOT_HDR + entry_real;        /* 128 */
    unsigned cap = rdsize;                               /* usable bytes */

    unsigned char *rdbuf, *base; unsigned maplen;
    make_guard_buf(cap, &rdbuf, &base, &maplen);

    u32 aoff = INDEXROOT_HDR;                            /* walk start */

    /* plant the entry header at rdbuf+aoff */
    struct iep_disk *ie = (struct iep_disk *)(rdbuf + aoff);
    ie->flag = SUBNODE;                                  /* trigger the dive */
    ie->reclen = (mode && strcmp(mode, "tiny") == 0) ? 2 :
                 (strcmp(mode, "clean") == 0) ? (u16)entry_real : 0xFFFF;
    rdbuf[aoff + 80] = 6;                                /* ie_fnamelen */
    memcpy(rdbuf + aoff + 82, "z\0z\0z\0z\0z\0z\0", 12); /* ie_fname "zzzzzz" */

    fprintf(stderr,
        "[harness] mode=%s apply_fix=%d cap=%u rdsize=%u aoff=%u "
        "entry_reclen=%u dive_off=%llu (vs rdsize=%u)\n",
        mode, apply_fix, cap, rdsize, aoff, ie->reclen,
        (unsigned long long)((uint64_t)aoff + ie->reclen - CN_SIZE), rdsize);

    faulted = 0;
    int rc;
    cn_t cn = 0;
    if (sigsetjmp(jb, 1) == 0) {
        rc = do_dive(rdbuf, rdsize, aoff, ie->reclen, apply_fix, &cn);
    } else {
        /* faulted inside the dive read -> OOB read past the buffer */
        printf("mode=%-6s apply_fix=%d -> rc=2  SIGSEGV -> OOB READ past rdbuf "
               "(dive offset %llu >= rdsize %u)\n",
               mode, apply_fix,
               (unsigned long long)((uint64_t)aoff + ie->reclen - CN_SIZE), rdsize);
        munmap(base, maplen);
        return 2;
    }

    if (rc == 0) {
        printf("mode=%-6s apply_fix=%d -> rc=0  dive read OK, cn=0x%016llx "
               "(in bounds)\n", mode, apply_fix,
               (unsigned long long)cn);
    } else {
        printf("mode=%-6s apply_fix=%d -> rc=-1 FIX REJECTED malformed entry "
               "(reclen=%u) -> EINVAL in kernel\n", mode, apply_fix, ie->reclen);
    }
    munmap(base, maplen);
    return (rc == 0) ? 0 : 1;
}
