/*
 * DF-0859 — Deterministic harness for missing ab_busycnt validation in
 *           hpfs_splitalsec() / hpfs_concatalsec() / hpfs_alblk2alsec()
 *           (sys/vfs/hpfs/hpfs_alsubr.c:225/229, :271, :314).
 *
 * These three helper routines are reached from the HPFS WRITE / TRUNCATE
 * path (hpfs_addextent → hpfs_alblk2alsec / hpfs_splitalsec,
 *  hpfs_truncatealblk → hpfs_concatalsec).  Their bcopy length arithmetic
 *  takes ab_busycnt verbatim from the on-disk alblk_t (u_int8_t — never
 *  validated) and uses it to size both the source offset and the byte count
 *  of a bcopy into a freshly allocated 512-byte alsec buffer.
 *
 * This harness is a FAITHFUL userspace transcription of the three bcopys
 * against the EXACT on-disk struct layouts from sys/vfs/hpfs/hpfs.h:
 *
 *   alblk_t  = 8 bytes  (ab_flag, ab_res[3], ab_freecnt, ab_busycnt, ab_freeoff)
 *   alleaf_t = 12 bytes (al_off, al_len, al_lsn)
 *   alnode_t = 8 bytes  (an_nextoff, an_lsn)
 *   fnode.fn_abd[0x60]  = 96-byte data area
 *   alsec.as_abd[0x1E0] = 480-byte data area
 *
 * Container layouts:
 *   - struct alsec is 4+4+4 (as_magic, as_self, as_parent) + 8 (as_ab) +
 *     0x1E0 (as_abd) = 500 bytes, allocated inside a DEV_BSIZE=512 buffer
 *     (getblk in hpfs_allocalsec, sys/vfs/hpfs/hpfs_alsubr.c:178).
 *   - The fnode fn_ab lives inside struct hpfsnode (kmalloc'd in
 *     hpfs_vfsops.c:488).  Its data area fn_abd is 96 bytes; the source
 *     bcopy in hpfs_alblk2alsec reads straight from fn_ab.
 *
 * Poisoned allocator: the destination container is placed at the END of the
 * first mmap'd page; the next page is poisoned with 0xAA.  Any byte written
 * past the container's data area lands in the 0xAA page, modelling a slab
 * neighbour the attacker does not own.
 *
 * Compile:  cc -O2 -o harness harness.c
 * Run:      ./harness
 *
 * Expected (BUG PRESENT):
 *   [BUG] alblk2alsec  busycnt=255 sz=12 (alleaf)  WRITTEN=3068B  OOB past alsec buffer=2568B  poison=HIT
 *   [BUG] alblk2alsec  busycnt=255 sz=8  (alnode)  WRITTEN=2048B  OOB past alsec buffer=1548B  poison=HIT
 *   [BUG] splitalsec   busycnt=255 sz=12 (alleaf)  WRITTEN=1524B  OOB past alsec buffer=1032B  poison=HIT
 *   [BUG] splitalsec   busycnt=255 sz=8  (alnode)  WRITTEN=1016B  OOB past alsec buffer=524B   poison=HIT
 *   [BUG] concatalsec  busycnt=255 sz=12 (alleaf)  WRITTEN=3060B  OOB past alsec buffer=2568B  poison=HIT
 *   [BUG] concatalsec  busycnt=255 sz=8  (alnode)  WRITTEN=2040B  OOB past alsec buffer=1548B  poison=HIT
 *
 * Expected (FIXED): all six cases → REJECTED (EINVAL, no bcopy run).
 */

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

/* ---- Exact struct layouts from sys/vfs/hpfs/hpfs.h --------------------- */

#define AB_NODES    0x80
#define AB_FNPARENT 0x20
#define AS_MAGIC    0x37E40AAE

typedef struct alblk {
    uint8_t  ab_flag;
    uint8_t  ab_res[3];
    uint8_t  ab_freecnt;
    uint8_t  ab_busycnt;       /* <-- attacker-controlled, UNVALIDATED */
    uint16_t ab_freeoff;
} alblk_t;                     /* sizeof = 8 */

typedef struct alleaf {
    uint32_t al_off;
    uint32_t al_len;
    uint32_t al_lsn;
} alleaf_t;                    /* sizeof = 12 */

typedef struct alnode {
    uint32_t an_nextoff;
    uint32_t an_lsn;
} alnode_t;                    /* sizeof = 8 */

/* fnode alloc-block data area (96 bytes) */
#define FN_ABD_SIZE   0x60
/* alsec alloc-block data area (480 bytes) */
#define AS_ABD_SIZE   0x1E0
/* alsec total = 12 (as_magic+as_self+as_parent) + 8 (as_ab) + 480 = 500 */
#define AS_TOTAL_SIZE (3*sizeof(uint32_t) + sizeof(alblk_t) + AS_ABD_SIZE)
/* buffer cache backing for an alsec is DEV_BSIZE = 512 */
#define DEV_BSIZE     512

/* Legitimate maximum busycnt per container (data area / element size) */
#define FN_MAX_LEAF   (FN_ABD_SIZE / sizeof(alleaf_t))   /*  8 */
#define FN_MAX_NODE   (FN_ABD_SIZE / sizeof(alnode_t))   /* 12 */
#define AS_MAX_LEAF   (AS_ABD_SIZE / sizeof(alleaf_t))   /* 40 */
#define AS_MAX_NODE   (AS_ABD_SIZE / sizeof(alnode_t))   /* 60 */

/* A struct that mimics just the head of struct fnode the way the source
 * bcopy in hpfs_alblk2alsec sees it (ab pointer + abd data area). */
typedef struct fnode_container {
    alblk_t  fn_ab;
    uint8_t  fn_abd[FN_ABD_SIZE];
} fnode_container_t;

typedef struct alsec_container {
    uint32_t as_magic;
    uint32_t as_self;
    uint32_t as_parent;
    alblk_t  as_ab;
    uint8_t  as_abd[AS_ABD_SIZE];
} alsec_container_t;           /* sizeof = 500 */

/* ---- Poisoned allocator ----------------------------------------------- */
/* Place the container at the END of the first page; the second page is
 * poisoned (0xAA).  Any write past the container's data area lands in the
 * poison page, modelling the slab-neighbour case. */
static void *
poison_alloc(size_t container_size, size_t pagesz, uint8_t poison)
{
    size_t total = pagesz * 2;
    void *m = mmap(NULL, total, PROT_READ|PROT_WRITE,
                   MAP_PRIVATE|MAP_ANON, -1, 0);
    if (m == MAP_FAILED) { perror("mmap"); exit(2); }
    /* poison the second page */
    memset((char *)m + pagesz, poison, pagesz);
    /* place container so its end is exactly at the page boundary */
    char *p = (char *)m + (pagesz - container_size);
    return p;
}

/* ---- BUG transcription: hpfs_alblk2alsec (sys/vfs/hpfs/hpfs_alsubr.c:297-322)
 *      bcopy(abp, nabp, sizeof(alblk_t) + sz * abp->ab_busycnt)  [line 314]
 *
 * Source = fnode fn_ab (FN_ABD_SIZE data area).  Destination = new alsec
 * as_ab (inside the 512-byte buffer-cache buffer).  Returns bytes written
 * past the end of the 512-byte backing buffer. */
static size_t
bug_alblk2alsec(alblk_t *abp_src /* fnode fn_ab */,
                alsec_container_t *nasp_dst /* new alsec in 512B buffer */,
                size_t *written_out, int *poison_hit)
{
    size_t sz = (abp_src->ab_flag & AB_NODES) ? sizeof(alnode_t)
                                              : sizeof(alleaf_t);
    alblk_t *nabp = &nasp_dst->as_ab;
    /* The kernel bcopy: */
    size_t n = sizeof(alblk_t) + sz * abp_src->ab_busycnt;
    *written_out = n;
    /* Destination starts at &nasp_dst->as_ab (offset 12 in the 512B buffer).
     * The buffer ends at offset DEV_BSIZE = 512.  Compute overrun. */
    char *dst_start = (char *)nabp;
    char *buf_end   = (char *)nasp_dst + DEV_BSIZE;
    /* emulate write: each byte from src to dst */
    char *dst_cur = dst_start;
    *poison_hit = 0;
    for (size_t i = 0; i < n; i++) {
        if (dst_cur >= buf_end) {
            if (*dst_cur == 0xAA) *poison_hit = 1;
        }
        *dst_cur = 0xCC;  /* mark "written" */
        dst_cur++;
    }
    size_t past = (size_t)((dst_cur > buf_end) ? (dst_cur - buf_end) : 0);
    return past;
}

/* ---- BUG transcription: hpfs_splitalsec (sys/vfs/hpfs/hpfs_alsubr.c:207-245)
 *      n1=(busycnt+1)/2; n2=busycnt-n1; sz=...
 *      bcopy(abp + sizeof(alblk_t) + n1*sz,  nabp + sizeof(alblk_t),  n2*sz)  [line 229]
 *
 * Source abp = asp->as_ab (alsec, 480B data area).  Destination nabp =
 * new alsec as_ab in 512B buffer. */
static size_t
bug_splitalsec(alblk_t *abp_src /* asp->as_ab */,
               alsec_container_t *nasp_dst /* new alsec in 512B buffer */,
               size_t *written_out, int *poison_hit, size_t *src_oob_out)
{
    int n1 = (abp_src->ab_busycnt + 1) / 2;
    int n2 = (abp_src->ab_busycnt - n1);
    size_t sz = (abp_src->ab_flag & AB_NODES) ? sizeof(alnode_t)
                                              : sizeof(alleaf_t);
    alblk_t *nabp = &nasp_dst->as_ab;
    /* kernel bcopy args: */
    char *src = (char *)abp_src + sizeof(alblk_t) + (size_t)n1 * sz;
    char *dst = (char *)nabp + sizeof(alblk_t);
    size_t n = (size_t)n2 * sz;
    *written_out = n;
    /* source OOB past as_abd (480 bytes data area): src+n vs abd_end */
    char *abd_end = (char *)abp_src + sizeof(alblk_t) + AS_ABD_SIZE;
    size_t src_oob = 0;
    if (src + n > abd_end) src_oob = (size_t)((src + n) - abd_end);
    *src_oob_out = src_oob;
    /* destination OOB past 512B backing buffer */
    char *buf_end = (char *)nasp_dst + DEV_BSIZE;
    char *dst_cur = dst;
    *poison_hit = 0;
    for (size_t i = 0; i < n; i++) {
        if (dst_cur >= buf_end) {
            if (*dst_cur == 0xAA) *poison_hit = 1;
        }
        *dst_cur = 0xCC;
        dst_cur++;
    }
    size_t past = (size_t)((dst_cur > buf_end) ? (dst_cur - buf_end) : 0);
    return past;
}

/* ---- BUG transcription: hpfs_concatalsec (sys/vfs/hpfs/hpfs_alsubr.c:257-288)
 *      if (ab0p->ab_freecnt > ab1p->ab_busycnt) {
 *          bcopy(AB_ALNODE(ab1p), AB_FREEANP(ab0p), ab1p->ab_busycnt * sz); [line 278]
 *      }
 *
 * Here we model ab0p as a fresh alsec with ab_freeoff=sizeof(alblk_t) (start
 * of data area) and ab_freecnt=0xFF (so the guard always passes — both
 * fields come from disk).  Source ab1p is another alsec.  The destination
 * is ab0p's as_abd, written from ab_freeoff onwards. */
static size_t
bug_concatalsec(alblk_t *ab0p, alsec_container_t *as0_container,
                alblk_t *ab1p,
                size_t *written_out, int *poison_hit, size_t *src_oob_out)
{
    size_t sz = (ab0p->ab_flag & AB_NODES) ? sizeof(alnode_t)
                                           : sizeof(alleaf_t);
    /* Caller sets ab0p->ab_freecnt and ab0p->ab_freeoff; we honour both.
     * The kernel guard at :271 uses these UNVALIDATED on-disk bytes. */
    if (!(ab0p->ab_freecnt > ab1p->ab_busycnt)) {
        *written_out = 0; *poison_hit = 0; *src_oob_out = 0;
        return 0;
    }
    /* kernel bcopy args:
     *   src = AB_ALNODE(ab1p) = ab1p + sizeof(alblk_t)
     *   dst = AB_FREEANP(ab0p) = ab0p + ab0p->ab_freeoff  (caller-set)
     *   n   = ab1p->ab_busycnt * sz
     */
    char *src = (char *)ab1p + sizeof(alblk_t);
    char *dst = (char *)ab0p + ab0p->ab_freeoff;
    size_t n = (size_t)ab1p->ab_busycnt * sz;
    *written_out = n;
    /* source OOB past ab1p's as_abd */
    char *abd_end = (char *)ab1p + sizeof(alblk_t) + AS_ABD_SIZE;
    size_t src_oob = 0;
    if (src + n > abd_end) src_oob = (size_t)((src + n) - abd_end);
    *src_oob_out = src_oob;
    /* destination OOB past 512B backing buffer of as0_container */
    char *buf_end = (char *)as0_container + DEV_BSIZE;
    char *dst_cur = dst;
    *poison_hit = 0;
    for (size_t i = 0; i < n; i++) {
        if (dst_cur >= buf_end) {
            if (*dst_cur == 0xAA) *poison_hit = 1;
        }
        *dst_cur = 0xCC;
        dst_cur++;
    }
    size_t past = (size_t)((dst_cur > buf_end) ? (dst_cur - buf_end) : 0);
    return past;
}

/* ---- FIXED transcription: validate ab_busycnt against container max ---- */
/* Returns 0 if the (forged) busycnt would be ACCEPTED by an unfixed kernel
 * (i.e. it passes the bounds check), or -1 if the fix REJECTS it.  For
 * forged=255 and any container, the fix rejects. */
static int
validate(int is_fnode, uint8_t ab_flag, uint8_t busycnt)
{
    size_t maxcnt = (ab_flag & AB_NODES)
        ? (is_fnode ? FN_MAX_NODE : AS_MAX_NODE)
        : (is_fnode ? FN_MAX_LEAF : AS_MAX_LEAF);
    /* mirror the proposed fix: reject when busycnt > maxcnt */
    return (busycnt > maxcnt) ? -1 : 0;
}

int main(void)
{
    size_t pagesz = sysconf(_SC_PAGESIZE);

    printf("=== DF-0859 deterministic OOB-WRITE proof ===\n");
    printf("struct sizes: alblk=%zu alleaf=%zu alnode=%zu\n",
           sizeof(alblk_t), sizeof(alleaf_t), sizeof(alnode_t));
    printf("containers : fnode fn_abd=0x%x  alsec as_abd=0x%x  alsec total=%zu  buffer-cache buf=%d\n",
           FN_ABD_SIZE, AS_ABD_SIZE, sizeof(alsec_container_t), DEV_BSIZE);
    printf("legit max  : fnode-leaves=%d fnode-nodes=%d "
           "alsec-leaves=%d alsec-nodes=%d\n\n",
           (int)FN_MAX_LEAF, (int)FN_MAX_NODE,
           (int)AS_MAX_LEAF, (int)AS_MAX_NODE);

    uint8_t forged = 255;
    int any_oob = 0, all_rejected = 1;
    size_t worst = 0;

    /* ---- hpfs_alblk2alsec : source = fnode fn_ab, dest = new alsec 512B buf */
    printf("--- hpfs_alblk2alsec (sys/vfs/hpfs/hpfs_alsubr.c:314) ---\n");
    printf("    bcopy(abp=fnode fn_ab, nabp=new alsec as_ab, sizeof(alblk_t)+sz*busycnt)\n");
    for (int nodes = 0; nodes <= 1; nodes++) {
        size_t sz = nodes ? sizeof(alnode_t) : sizeof(alleaf_t);
        void *fnbase = poison_alloc(sizeof(fnode_container_t), pagesz, 0xAA);
        fnode_container_t *fn = (fnode_container_t *)fnbase;
        fn->fn_ab.ab_flag = nodes ? AB_NODES : 0;
        fn->fn_ab.ab_busycnt = forged;
        /* destination: a fresh 512B alsec buffer (modelled as alsec_container
         * placed at the end of a poison-tailed page; the backing malloc in
         * the kernel is exactly DEV_BSIZE). */
        void *asbase = poison_alloc(sizeof(alsec_container_t), pagesz, 0xAA);
        alsec_container_t *as = (alsec_container_t *)asbase;
        size_t written; int poison;
        size_t oob = bug_alblk2alsec(&fn->fn_ab, as, &written, &poison);
        printf("[BUG] alblk2alsec  busycnt=%-3u sz=%2zu (%s)  WRITTEN=%zuB  OOB past 512B buf=%zuB  poison=%s\n",
               forged, sz, nodes?"alnode":"alleaf", written, oob, poison?"HIT":"no");
        if (oob > 0) any_oob = 1;
        if (oob > worst) worst = oob;
        if (validate(/*is_fnode=*/1, fn->fn_ab.ab_flag, forged) == 0)
            all_rejected = 0;
    }
    printf("\n");

    /* ---- hpfs_splitalsec : source = old alsec, dest = new alsec 512B buf */
    printf("--- hpfs_splitalsec (sys/vfs/hpfs/hpfs_alsubr.c:225/229) ---\n");
    printf("    n1=(busycnt+1)/2 n2=busycnt-n1 ; bcopy(abp+8+n1*sz, nabp+8, n2*sz)\n");
    for (int nodes = 0; nodes <= 1; nodes++) {
        size_t sz = nodes ? sizeof(alnode_t) : sizeof(alleaf_t);
        /* old alsec: doesn't matter for dest OOB; just need its alblk */
        void *oldbase = poison_alloc(sizeof(alsec_container_t), pagesz, 0xBB);
        alsec_container_t *old = (alsec_container_t *)oldbase;
        old->as_ab.ab_flag = nodes ? AB_NODES : 0;
        old->as_ab.ab_busycnt = forged;
        /* new alsec 512B buffer */
        void *newbase = poison_alloc(sizeof(alsec_container_t), pagesz, 0xAA);
        alsec_container_t *nw = (alsec_container_t *)newbase;
        size_t written, src_oob; int poison;
        size_t oob = bug_splitalsec(&old->as_ab, nw, &written, &poison, &src_oob);
        printf("[BUG] splitalsec   busycnt=%-3u sz=%2zu (%s)  WRITTEN=%zuB  OOB past 512B buf=%zuB  src OOB past as_abd=%zuB  poison=%s\n",
               forged, sz, nodes?"alnode":"alleaf", written, oob, src_oob, poison?"HIT":"no");
        if (oob > 0) any_oob = 1;
        if (oob > worst) worst = oob;
        if (validate(/*is_fnode=*/0, old->as_ab.ab_flag, forged) == 0)
            all_rejected = 0;
    }
    printf("\n");

    /* ---- hpfs_concatalsec : source = ab1p alsec, dest = ab0p alsec */
    printf("--- hpfs_concatalsec (sys/vfs/hpfs/hpfs_alsubr.c:271/278) ---\n");
    printf("    if(ab0.freecnt>ab1.busycnt) bcopy(AB_ALNODE(ab1), AB_FREEANP(ab0), ab1.busycnt*sz)\n");
    /* The guard at :271 is `ab0p->ab_freecnt > ab1p->ab_busycnt` — BOTH
     * fields are untrusted on-disk bytes (DF-0859).  We pick the maximally
     * overflowing combination: ab0.freecnt = 0xFF (max u8), ab1.busycnt =
     * a value smaller than 0xFF but still > alsec max (so the bcopy
     * overflows the destination data area).  We use 0xFE so the guard is
     * `0xFF > 0xFE` = TRUE and the bcopy writes 0xFE*sz bytes. */
    uint8_t concat_busycnt = 0xFE;     /* 254 — passes the forged guard */
    for (int nodes = 0; nodes <= 1; nodes++) {
        size_t sz = nodes ? sizeof(alnode_t) : sizeof(alleaf_t);
        /* ab0 = destination alsec (512B buf) */
        void *ab0base = poison_alloc(sizeof(alsec_container_t), pagesz, 0xAA);
        alsec_container_t *ab0c = (alsec_container_t *)ab0base;
        ab0c->as_ab.ab_flag = nodes ? AB_NODES : 0;
        /* ab0's freecnt is FORGED to 0xFF so the guard passes regardless;
         * ab_freeoff forged to sizeof(alblk_t) so the bcopy destination
         * starts at the beginning of the data area (worst case). */
        ab0c->as_ab.ab_freecnt = 0xFF;
        ab0c->as_ab.ab_freeoff = sizeof(alblk_t);
        /* ab1 = source alsec */
        void *ab1base = poison_alloc(sizeof(alsec_container_t), pagesz, 0xBB);
        alsec_container_t *ab1c = (alsec_container_t *)ab1base;
        ab1c->as_ab.ab_flag = nodes ? AB_NODES : 0;
        ab1c->as_ab.ab_busycnt = concat_busycnt;
        size_t written, src_oob; int poison;
        size_t oob = bug_concatalsec(&ab0c->as_ab, ab0c, &ab1c->as_ab,
                                     &written, &poison, &src_oob);
        printf("[BUG] concatalsec  ab1.busycnt=%-3u ab0.freecnt=%-3u sz=%2zu (%s)  WRITTEN=%zuB  OOB past 512B buf=%zuB  src OOB past as_abd=%zuB  poison=%s\n",
               concat_busycnt, 0xFF, sz, nodes?"alnode":"alleaf", written, oob, src_oob, poison?"HIT":"no");
        if (oob > 0) any_oob = 1;
        if (oob > worst) worst = oob;
        /* fix: validate ab0.busycnt AND ab1.busycnt; the guard's use of
         * freecnt is also covered because the fix caps freecnt implicitly
         * by capping busycnt (ab_freecnt = max - busycnt). */
        if (validate(/*is_fnode=*/0, ab1c->as_ab.ab_flag, concat_busycnt) == 0)
            all_rejected = 0;
    }
    printf("\n");

    /* ---- FIXED mode: validate rejects forged busycnt ----------------- */
    printf("--- FIXED: validate ab_busycnt against container max before bcopy ---\n");
    /* Note: validate() returns 0 if the value passes (would be ACCEPTED by
     * an unfixed kernel) and -1 if the fix REJECTS it (out-of-range).  For
     * forged busycnt=255 the fix always rejects. */
    int rc;
    rc = validate(/*is_fnode=*/1, 0,        forged);
    printf("[FIX] alblk2alsec  leaf  forged busycnt=%u -> %s\n", forged, (rc==0)?"accepted (BUG)":"REJECTED (EINVAL)");
    if (rc == 0) all_rejected = 0;
    rc = validate(/*is_fnode=*/1, AB_NODES, forged);
    printf("[FIX] alblk2alsec  node  forged busycnt=%u -> %s\n", forged, (rc==0)?"accepted (BUG)":"REJECTED (EINVAL)");
    if (rc == 0) all_rejected = 0;
    rc = validate(/*is_fnode=*/0, 0,        forged);
    printf("[FIX] split/concat leaf  forged busycnt=%u -> %s\n", forged, (rc==0)?"accepted (BUG)":"REJECTED (EINVAL)");
    if (rc == 0) all_rejected = 0;
    rc = validate(/*is_fnode=*/0, AB_NODES, forged);
    printf("[FIX] split/concat node  forged busycnt=%u -> %s\n", forged, (rc==0)?"accepted (BUG)":"REJECTED (EINVAL)");
    if (rc == 0) all_rejected = 0;

    printf("\n=== SUMMARY ===\n");
    printf("DF_0859_BUG_OOB_WRITE_MAX_BYTES=%zu\n", worst);
    printf("DF_0859_BUG_CONFIRMED=%d\n", any_oob);
    printf("DF_0859_FIX_REJECTS_FORGED_BUSYCNT=%d\n", all_rejected);
    return any_oob ? 0 : 1;
}
