/*
 * DF-0844 — Userspace harness replicating the dirhash-build OOB read.
 *
 * Reproduces the missing bounds check in ufsdirhash_build()
 * (sys/vfs/ufs/ufs_dirhash.c lines 200-216):
 *
 *   ep = (struct direct *)((char *)bp->b_data + (pos & bmask));
 *   if (ep->d_reclen == 0 || ep->d_reclen >
 *       DIRBLKSIZ - (pos & (DIRBLKSIZ - 1))) {
 *       // Corrupted directory — REJECTED
 *       goto fail;
 *   }
 *   // *** MISSING: d_reclen >= DIRSIZ(NEWDIRFMT, ep) ***
 *   if (ep->d_ino != 0) {
 *       slot = ufsdirhash_hash(dh, ep->d_name, ep->d_namlen);  // OOB READ
 *   }
 *
 * The existing check only validates the entry fits within its 512-byte
 * DIRBLKSIZ chunk. It does NOT validate that d_reclen is large enough to
 * hold d_name (i.e. d_reclen >= DIRSIZ(0, ep)). A crafted entry with
 * d_reclen=8 (passes the chunk check when at chunk offset 504) but
 * d_namlen=255 causes ufsdirhash_hash → fnv_32_buf to read 255 bytes
 * from ep->d_name, which extends past the buffer bp->b_data into kernel
 * heap — a CWE-125 out-of-bounds read.
 *
 * This harness places the crafted entry at the exact tail of a page,
 * with a PROT_NONE guard page immediately after. When the replicated
 * dirhash loop reads ep->d_name (255 bytes), it faults into the guard
 * page → SIGSEGV = deterministic proof of the OOB read.
 *
 * Build: cc -O2 -o dirhash_oob dirhash_oob.c
 * Run:   ./dirhash_oob            (expects SIGSEGV = OOB confirmed)
 *        ./dirhash_oob --fixed    (expects clean exit = fix rejects entry)
 */

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

/* ---- Constants from sys/vfs/ufs/dir.h (DragonFlyBSD) ---- */
#define DEV_BSIZE     512
#define DIRBLKSIZ     DEV_BSIZE    /* 512 */
#define MAXNAMLEN     255
#define DIRALIGN      4

/* On-disk directory entry (little-endian DragonFly). */
struct direct {
    uint32_t d_ino;            /* 0 */
    uint16_t d_reclen;         /* 4 */
    uint8_t  d_type;           /* 6 */
    uint8_t  d_namlen;         /* 7 */
    char     d_name[MAXNAMLEN + 1]; /* 8 */
};

/* offsetof(struct direct, d_name) = 8 */
#define DIRECT_HDRSZ  8

#define DIRECTSIZ(namlen) \
    ((DIRECT_HDRSZ + ((namlen)+1) + 3) & ~3)
#define DIRSIZ(dp)  DIRECTSIZ((dp)->d_namlen)

/* ---- FNV-1a hash (from sys/libkern/fnv_hash.h) ---- */
#define FNV1_32_INIT 0x811c9dc5U
static uint32_t
fnv_32_buf(const void *buf, size_t len, uint32_t hval)
{
    const uint8_t *s = (const uint8_t *)buf;
    while (len--)
        hval *= 0x01000193U, hval ^= *s++;
    return hval;
}

/* Replicated from ufsdirhash_hash() */
static volatile uint32_t g_sink;
static uint32_t
dirhash_hash(const char *name, int namelen)
{
    uint32_t hash;
    hash = fnv_32_buf(name, namelen, FNV1_32_INIT);
    g_sink = hash;  /* prevent DCE */
    return hash;
}

/* ---- Signal handler for OOB detection ---- */
static sigjmp_buf oob_jmp;
static volatile int oob_triggered = 0;

static void
sighandler(int sig)
{
    oob_triggered = 1;
    siglongjmp(oob_jmp, 1);
}

/*
 * Replicate the dirhash-build entry-processing loop for a single entry
 * at the tail of a buffer. Returns 0 if entry accepted & hashed (no OOB),
 * -1 if entry rejected by the bounds check.
 */
static int
process_entry(const struct direct *ep, int fixed_check)
{
    /* The EXISTING check from ufs_dirhash.c:201-206 */
    if (ep->d_reclen == 0 ||
        ep->d_reclen > DIRBLKSIZ - (DIRBLKSIZ - 8 /* simulated chunk tail */)) {
        return -1;
    }

    /* THE FIX: d_reclen >= DIRSIZ(ep) — currently MISSING in the kernel */
    if (fixed_check && ep->d_reclen < DIRSIZ(ep)) {
        return -1;
    }

    /* ufsdirhash_build line 207-209: if d_ino != 0, hash the name */
    if (ep->d_ino != 0) {
        /* This reads ep->d_namlen bytes from ep->d_name.
         * If d_name extends past the buffer → OOB READ. */
        uint32_t h = dirhash_hash(ep->d_name, ep->d_namlen);
        g_sink ^= h;  /* use result */
    }
    return 0;
}

int
main(int argc, char **argv)
{
    int fixed = (argc > 1 && strcmp(argv[1], "--fixed") == 0);
    long pagesize = (long)getpagesize();
    if (pagesize <= 0)
        pagesize = 4096;

    /*
     * Allocate two consecutive pages. The first page holds the
     * directory data buffer at its tail; the second page is a
     * PROT_NONE guard. The crafted entry's d_name will land in
     * the guard page → SIGSEGV when read.
     */
    size_t mapsz = (size_t)pagesize * 2;
    char *base = mmap(NULL, mapsz, PROT_READ | PROT_WRITE,
                      MAP_PRIVATE | MAP_ANON, -1, 0);
    if (base == MAP_FAILED) {
        perror("mmap");
        return 2;
    }
    char *guard = base + pagesize;
    if (mprotect(guard, pagesize, PROT_NONE) != 0) {
        perror("mprotect");
        return 2;
    }

    /*
     * Place the crafted entry at the tail of the first page.
     *
     * We want the entry positioned so that:
     *   - pos & (DIRBLKSIZ-1) = 504  (so d_reclen <= 512-504 = 8)
     *   - ep->d_name starts exactly at the page boundary (guard page)
     *
     * Entry header is 8 bytes (DIRECT_HDRSZ). If we place ep at
     * page_offset = pagesize - 8, then d_name starts at pagesize,
     * which is the first byte of the guard page.
     *
     * For the chunk check: pos & (DIRBLKSIZ-1) must be 504.
     * (pagesize - 8) & 511 = ?
     *   pagesize=4096: 4088 & 511 = 4088 mod 512 = 4088 - 7*512 = 504. ✓
     */
    int entry_pageoff = pagesize - DIRECT_HDRSZ;  /* e.g. 4088 */
    int chunk_off = entry_pageoff & (DIRBLKSIZ - 1);

    printf("DF-0844 dirhash OOB harness\n");
    printf("  pagesize      = %ld\n", pagesize);
    printf("  entry offset  = %d (page-relative)\n", entry_pageoff);
    printf("  chunk offset  = %d\n", chunk_off);
    printf("  max d_reclen  = %d (DIRBLKSIZ - chunk_off)\n",
           DIRBLKSIZ - chunk_off);

    struct direct *ep = (struct direct *)(base + entry_pageoff);

    /* Craft the malformed entry:
     *   d_reclen = 8      → passes chunk check (8 <= 8)
     *   d_namlen = 255    → but entry only holds 0 name bytes!
     *   d_ino    = 1      → non-zero → triggers ufsdirhash_hash
     *   d_type   = DT_REG(8)
     *
     * ep->d_name starts at base + pagesize = guard page.
     * dirhash_hash will read 255 bytes from the guard page → OOB.
     */
    ep->d_ino    = 1;
    ep->d_reclen = 8;
    ep->d_type   = 8;     /* DT_REG */
    ep->d_namlen = MAXNAMLEN;  /* 255 */

    printf("  crafted entry: d_ino=%u d_reclen=%u d_type=%u d_namlen=%u\n",
           ep->d_ino, ep->d_reclen, ep->d_type, ep->d_namlen);
    printf("  DIRSIZ(ep)    = %u  (d_reclen %u < DIRSIZ → MALFORMED)\n",
           DIRSIZ(ep), ep->d_reclen);
    printf("  d_name starts at offset %d = %s\n",
           entry_pageoff + DIRECT_HDRSZ,
           (entry_pageoff + DIRECT_HDRSZ == pagesize) ?
             "GUARD PAGE BOUNDARY" : "inside page");
    printf("  mode          = %s\n", fixed ? "FIXED (with DIRSIZ check)" : "BUGGY (no DIRSIZ check)");

    /* Install SIGSEGV handler to catch the OOB read cleanly */
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = sighandler;
    sa.sa_flags = SA_NODEFER;
    sigaction(SIGSEGV, &sa, NULL);
    sigaction(SIGBUS, &sa, NULL);

    printf("\nProcessing entry (replicating ufsdirhash_build loop)...\n");

    if (sigsetjmp(oob_jmp, 1) == 0) {
        int rc = process_entry(ep, fixed);
        if (rc == -1) {
            printf("RESULT: Entry REJECTED by bounds check (this is the FIX behavior).\n");
            printf("        The malformed entry was correctly detected and rejected.\n");
            printf("        No OOB read occurred.\n");
        } else {
            printf("RESULT: Entry ACCEPTED and hashed successfully (no OOB in this run).\n");
            printf("        WARNING: d_name was within valid memory by chance.\n");
        }
    } else {
        /* SIGSEGV/SIGBUS caught = OOB read into guard page */
        printf("RESULT: *** OOB READ CONFIRMED ***\n");
        printf("        fnv_32_buf(ep->d_name, 255) read past the buffer into the guard page.\n");
        printf("        The missing d_reclen >= DIRSIZ(ep) check allowed a 255-byte\n");
        printf("        out-of-bounds read from the entry's d_name field.\n");
        printf("        In the kernel this reads %d bytes of adjacent kernel heap.\n",
               ep->d_namlen);
    }

    munmap(base, mapsz);
    return oob_triggered ? 0 : (fixed ? 0 : 1);
}
