/*
 * harness.c - Deterministic DF-0769 production-primitive proof.
 *
 * Transcribes the EXACT vulnerable arithmetic from
 *   sys/vfs/hammer/hammer_vnops.c:1728-1738  (hammer_vop_readdir)
 *   sys/kern/vfs_subr.c:2560-2577           (vop_write_dirent)
 * and demonstrates the OOB kernel-heap read that occurs on a PRODUCTION
 * kernel (INVARIANTS OFF, where the KKASSERT at :1728 is compiled out).
 *
 * On GENERIC (INVARIANTS ON) the KKASSERT(data_len > 16) panics first, so
 * this primitive is never reached -- that is the GENERIC manifestation.
 * On production kernels the assertion is absent and the code proceeds:
 *
 *     d_namlen = (uint16_t)(data_len - HAMMER_ENTRY_NAME_OFF)
 *              = (uint16_t)(8 - 16) = (uint16_t)(-8) = 65528
 *
 * then in vop_write_dirent:
 *     bcopy(d_name, dp->d_name, 65528);
 *
 * reads 65528 bytes starting at cursor.data->entry.name, which lives inside
 * a 16KB hammer_buffer->ondisk allocation. offset of name within that buffer
 * is 16, so the bcopy reads (16384-16)=16368 valid bytes then ~49160 bytes
 * BEYOND the buffer = OOB kernel-heap read of neighbouring slab/page data.
 *
 * This harness reproduces that bcopy against a "poisoned" 16KB allocation
 * (filled with recognisable sentinel bytes simulating adjacent kernel heap)
 * so the OOB read extent is observable and deterministic.
 *
 * Build:  cc -O2 -o harness harness.c
 * Run:    ./harness
 */
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#define HAMMER_ENTRY_NAME_OFF 16
#define HAMMER_BUFSIZE        16384   /* hammer_buffer ondisk size */

/* mirror of vop_write_dirent's narrowing of the length argument */
static uint16_t forge_d_namlen(int32_t data_len)
{
    /* vnops.c:1737 -- the expression is computed in int, then passed to a
     * uint16_t parameter (implicit narrowing). */
    int expr = data_len - HAMMER_ENTRY_NAME_OFF;   /* 8 - 16 = -8 */
    return (uint16_t)expr;                          /* 65528 */
}

int main(void)
{
    /* ---- simulate the kernel allocator: a 16KB hammer_buffer->ondisk ---- */
    uint8_t *ondisk = (uint8_t *)malloc(HAMMER_BUFSIZE);
    /* the direntry data begins at the start of the buffer; name at +16 */
    memset(ondisk, 0xAA, HAMMER_BUFSIZE);
    memcpy(ondisk, "OBJIDXXXX", 8);          /* direntry obj_id (8 bytes) */
    memcpy(ondisk + HAMMER_ENTRY_NAME_OFF, "X", 1);  /* the 1-char name */

    /* ---- simulate adjacent kernel heap (what the OOB read will sweep) ----
     * In the real kernel the bytes immediately past hammer_buffer->ondisk
     * are whatever the slab allocator placed there (other kmalloc'd objects,
     * cred pointers, etc.). We tag successive regions so the leak is visible. */
    size_t poison_sz = 70000;
    uint8_t *poison  = (uint8_t *)malloc(poison_sz);
    memset(poison, 0xCC, poison_sz);
    /* the real buffer is 16KB; everything past it is "neighbouring heap" */
    memcpy(poison, ondisk, HAMMER_BUFSIZE);   /* first 16KB is the real buffer */
    free(ondisk);
    /* 'poison' now models the kernel address space: [0..16384) = ondisk buffer,
     * [16384..70000) = adjacent kernel heap that the bcopy will read OOB. */

    int32_t forged_data_len = 8;   /* what the crafted B-tree leaf carries */
    uint16_t d_namlen = forge_d_namlen(forged_data_len);
    printf("forged data_len          = %d\n", forged_data_len);
    printf("HAMMER_ENTRY_NAME_OFF    = %d\n", HAMMER_ENTRY_NAME_OFF);
    printf("data_len - NAME_OFF      = %d\n", forged_data_len - HAMMER_ENTRY_NAME_OFF);
    printf("=> d_namlen (uint16_t)   = %u   (0x%x)\n", d_namlen, d_namlen);

    /* source pointer == cursor.data->entry.name (offset 16 within buffer) */
    const char *d_name = (const char *)(poison + HAMMER_ENTRY_NAME_OFF);

    /* destination == freshly-allocated dirent (M_ZERO'd), as in vop_write_dirent */
    size_t len = ((16 /*offsetof(d_name)*/ + d_namlen + 1 + 7) & ~(size_t)7); /* _DIRENT_RECLEN */
    printf("=> _DIRENT_RECLEN(d_namlen) = %zu bytes (kmalloc'd + uiomove'd to user)\n", len);
    uint8_t *dp_d_name = (uint8_t *)calloc(1, d_namlen);

    /* the vulnerable bcopy -- reads 65528 bytes from d_name */
    memcpy(dp_d_name, d_name, d_namlen);

    /* measure the OOB extent: how far past the 16KB buffer does the read go? */
    size_t valid_in_buf = HAMMER_BUFSIZE - HAMMER_ENTRY_NAME_OFF;   /* 16368 */
    long oob;
    if (d_namlen > valid_in_buf)
        oob = (long)d_namlen - (long)valid_in_buf;
    else
        oob = 0;

    printf("\nvop_write_dirent bcopy(d_name, dp->d_name, %u)\n", d_namlen);
    printf("  source region:  16KB hammer_buffer->ondisk (name at +%d)\n",
           HAMMER_ENTRY_NAME_OFF);
    printf("  valid bytes in source buffer  = %zu\n", valid_in_buf);
    printf("  bytes read by bcopy           = %u\n", d_namlen);
    printf("  >>> OOB READ beyond buffer    = %ld bytes <<<\n", oob);
    printf("  uiomove then copies all %zu bytes (incl OOB data) to the getdents user buffer\n", len);
    printf("  => ~%ld-byte kernel-heap INFO LEAK via unprivileged getdents\n", oob);

    /* show a few bytes that came from past the buffer (the "leaked" bytes) */
    size_t leak_start = valid_in_buf;      /* index into dp_d_name past the buffer */
    printf("\nSample leaked bytes (from dp_d_name[%zu..], i.e. past the hammer buffer):\n  ",
           leak_start);
    for (size_t i = 0; i < 16 && leak_start + i < d_namlen; i++)
        printf("%02x ", dp_d_name[leak_start + i]);
    printf("\n(0xCC = adjacent kernel heap sentinel; real kernel exposes live heap data)\n");

    free(dp_d_name);
    free(poison);
    return 0;
}
