"""Strict semantic comparator for COFF-i386 function COMDAT sections. Vendored from the independently reviewed glm-coff-tooling repository at commit 117ad3c. Ambiguous relocation identity is deliberately non-equal. """ import hashlib import json import struct from pathlib import Path # ── COFF constants ────────────────────────────────────────────────── IMAGE_REL_I386_DIR32 = 0x0006 IMAGE_REL_I386_REL32 = 0x0014 IMAGE_SCN_CNT_CODE = 0x00000020 IMAGE_SCN_CNT_UNINITIALIZED_DATA = 0x00000080 IMAGE_SYM_CLASS_EXTERNAL = 0x02 IMAGE_SYM_ABSOLUTE = -1 IMAGE_SYM_UNDEFINED = 0 COFF_HEADER_SIZE = 20 SYMBOL_ENTRY_SIZE = 18 RELOC_ENTRY_SIZE = 10 SECTION_HEADER_SIZE = 40 # ── helpers ───────────────────────────────────────────────────────── def _c_string(data, strtab_off, strtab_len, name_off): if name_off < 4 or name_off >= strtab_len: raise CoffError("invalid string table offset for long name") abs_off = strtab_off + name_off if abs_off >= len(data): raise CoffError("invalid string table offset for long name") end = data.find(b"\0", abs_off) strtab_end = strtab_off + strtab_len if end == -1 or end >= strtab_end: raise CoffError("string table entry missing NUL terminator") return data[abs_off:end].decode("ascii", "replace") class CoffError(ValueError): pass def load(data): if isinstance(data, (str, Path)): data = Path(data).read_bytes() if len(data) < COFF_HEADER_SIZE: raise CoffError("truncated COFF header") machine, section_count, _, symbol_offset, symbol_count, optional_size, _ = \ struct.unpack_from(" len(data): raise CoffError("symbol table truncated") strtab_len = 0 if string_offset < len(data): if string_offset + 4 > len(data): raise CoffError("string table length field missing") strtab_len = struct.unpack_from(" len(data): raise CoffError("invalid string table length") sections = [] offset = COFF_HEADER_SIZE for index in range(section_count): if offset + SECTION_HEADER_SIZE > len(data): raise CoffError("section header truncated") raw_name, _, _, size, raw_offset, reloc_offset, _, reloc_count, _, flags = \ struct.unpack_from("<8sLLLLLLHHL", data, offset) sections.append({ "index": index + 1, "name": raw_name.rstrip(b"\0").decode("ascii", "replace"), "size": size, "raw": raw_offset, "reloc": reloc_offset, "reloc_count": reloc_count, "flags": flags, }) offset += SECTION_HEADER_SIZE # COFF represents uninitialized sections (normally .bss) with a # logical size but no file-backed payload. MSVC emits # PointerToRawData == 0 for these sections, so their logical size can # legitimately exceed the object-file length. uninitialized = bool(flags & IMAGE_SCN_CNT_UNINITIALIZED_DATA) if not (uninitialized and raw_offset == 0) and raw_offset + size > len(data): raise CoffError(f"section {index + 1} raw data extends past file end") if reloc_count and reloc_offset + reloc_count * RELOC_ENTRY_SIZE > len(data): raise CoffError(f"section {index + 1} relocation table truncated") symbols = [] by_index = {} idx = 0 while idx < symbol_count: entry_offset = symbol_offset + idx * SYMBOL_ENTRY_SIZE if entry_offset + SYMBOL_ENTRY_SIZE > len(data): raise CoffError("symbol table entry truncated") zeroes, name_off = struct.unpack_from(" 0 and idx + 1 + aux_count > symbol_count: raise CoffError("auxiliary symbol entries extend past symbol table") sym = {"index": idx, "name": name, "value": value, "section": section_no, "type": sym_type, "storage": storage} symbols.append(sym) by_index[idx] = sym idx += aux_count + 1 return {"data": data, "sections": sections, "symbols": symbols, "by_index": by_index} def symbol(obj, name): for item in obj["symbols"]: if item["name"] == name and item["section"] > 0: return item raise CoffError(f"section-relative symbol {name!r} not found") def _infer_section_base(obj, fn_section_index): """Try to infer the image-base VA of *fn_section_index* by finding a symbol defined both as absolute and as section-relative in that section. Returns the inferred base or *None* when no such pair exists or when duplicate names make the proof ambiguous.""" abs_syms = {} rel_syms = {} for s in obj["symbols"]: if s["section"] == IMAGE_SYM_ABSOLUTE: if s["name"] in abs_syms: return None abs_syms[s["name"]] = s["value"] elif s["section"] == fn_section_index: if s["name"] in rel_syms: return None rel_syms[s["name"]] = s["value"] for name, rel_val in rel_syms.items(): abs_val = abs_syms.get(name) if abs_val is not None and abs_val >= rel_val: base = abs_val - rel_val for name2, rel_val2 in rel_syms.items(): abs_val2 = abs_syms.get(name2) if abs_val2 is not None and (abs_val2 - rel_val2) != base: return None return base return None def _find_matching_rel_sym(obj, target, fn_sec_num, base_va): """Return the unique section-relative symbol that proves *target* (an absolute symbol) is a label within *fn_sec_num*. Requirements: - Exactly one absolute symbol with this name. - Exactly one section-relative symbol with this name, matching storage class and type. - Its value is consistent with *base_va*: abs_value - rel_value == base_va. Returns the matching section-relative symbol or *None* when proof is insufficient.""" abs_all = [s for s in obj["symbols"] if s["name"] == target["name"] and s["section"] == IMAGE_SYM_ABSOLUTE] if len(abs_all) != 1: return None rel_all = [s for s in obj["symbols"] if s["name"] == target["name"] and s["section"] == fn_sec_num and s["storage"] == target["storage"] and s["type"] == target["type"]] if len(rel_all) != 1: return None if target["value"] - rel_all[0]["value"] != base_va: return None return rel_all[0] def _section_bytes(obj, section): """Return the logical bytes of a COFF section. IMAGE_SCN_CNT_UNINITIALIZED_DATA sections may have a nonzero logical size with PointerToRawData == 0. In that encoding the bytes are zero-filled by the loader; offset zero is the COFF header, not section data. """ if (section["flags"] & IMAGE_SCN_CNT_UNINITIALIZED_DATA) \ and section["raw"] == 0: return bytearray(section["size"]) sec_end = section["raw"] + section["size"] if sec_end > len(obj["data"]): raise CoffError( f"section {section['index']} raw data extends past file end") return bytearray(obj["data"][section["raw"]:sec_end]) def _defined_noncode_destination(obj, source_section_number, target, addend): """Return a proven destination in another defined non-code section. csplit can encode a reference as an externally visible section anchor plus an addend while MSVC uses a local symbol at the destination. Those forms are equivalent only when the nearest externally visible anchor at or below the destination is unique. Section numbers alone are object-local and therefore are never used as cross-object identity. Ambiguous ownership, duplicate owner names, code sections, and destinations outside the target section deliberately remain symbolic. """ target_section_number = target["section"] if target_section_number <= 0 \ or target_section_number == source_section_number \ or target_section_number > len(obj["sections"]): return None section = obj["sections"][target_section_number - 1] if section["flags"] & IMAGE_SCN_CNT_CODE: return None destination_offset = target["value"] + addend if not 0 <= destination_offset <= section["size"]: return None anchors = [ item for item in obj["symbols"] if item["section"] == target_section_number and item["value"] <= destination_offset and item["storage"] == IMAGE_SYM_CLASS_EXTERNAL and item["type"] != 0x20 ] if not anchors: return None nearest_value = max(item["value"] for item in anchors) nearest = [item for item in anchors if item["value"] == nearest_value] if len(nearest) != 1: return None anchor = nearest[0] defined_with_anchor_name = [ item for item in obj["symbols"] if item["section"] > 0 and item["name"] == anchor["name"] ] if len(defined_with_anchor_name) != 1: return None return [ "defined-noncode", section["name"], anchor["name"], destination_offset - anchor["value"], ] def section_info(obj, function_name): fn = symbol(obj, function_name) return section_info_by_number(obj, fn["section"]) def section_info_by_number(obj, fn_sec_num): """Describe one COFF section using the hardened relocation resolver. ``section_info`` remains the function-name convenience API. Regression manifests also need identical, non-divergent semantics for data, BSS, directive, and debug sections, which may not have a unique public owner symbol. This section-number entry point deliberately shares every proof rule with the function comparator. """ if fn_sec_num <= 0 or fn_sec_num > len(obj["sections"]): raise CoffError(f"invalid section number {fn_sec_num}") section = obj["sections"][fn_sec_num - 1] raw = _section_bytes(obj, section) relocs = [] base_va = _infer_section_base(obj, fn_sec_num) for ri in range(section["reloc_count"]): roff = section["reloc"] + ri * RELOC_ENTRY_SIZE address, target_index, reloc_type = \ struct.unpack_from(" section["size"]: raise CoffError( f"relocation address {address} outside section {fn_sec_num} size {section['size']}") addend = struct.unpack_from(" section["size"]: raise CoffError( f"relocation address {address} outside section " f"{section_number} size {section['size']}") target = obj["by_index"][target_index] addend = struct.unpack_from("