"""physobj_owner_scan.py Identify owners of leaked CPhysicsObj instances (EoR vtable 0x007c78e0). Method: 1. Enumerate all CPhysicsObj instances by scanning RW memory for DWORD = 0x007c78e0 at offset 0 of any heap-aligned 4-byte slot. Each hit's address is a CPhysicsObj*. 2. For each instance, scan ALL committed RW memory for DWORDs equal to that CPhysicsObj's base address (these are owners holding strong refs). 3. For each owner-pointer hit, walk BACKWARDS up to 0x400 bytes within the same region looking for a DWORD that points into image memory (the owner's vtable). 4. Histogram (vtable, field_offset) — these are the leak holders. Output: ranked owner vtables and ranked (vtable, field_offset) pairs. """ import struct, sys from collections import Counter, defaultdict from minidump.minidumpfile import MinidumpFile CPHYSOBJ_VT = 0x007c78e0 def _ei(v): if v is None: return 0 if hasattr(v, 'value'): return int(v.value) return int(v) def main(): md = MinidumpFile.parse(sys.argv[1]) reader = md.get_reader().get_buffered_reader() mods = [] for m in md.modules.modules: mods.append((m.baseaddress, m.size, m.name)) def mod_of(addr): for b, s, n in mods: if b <= addr < b + s: return n.split("\\")[-1] return None image_ranges = [] for r in md.memory_info.infos: st, ty = _ei(r.State), _ei(r.Type) if st == 0x1000 and ty == 0x1000000: image_ranges.append((r.BaseAddress, r.BaseAddress + r.RegionSize)) image_ranges.sort() def is_image(addr): for lo, hi in image_ranges: if lo <= addr < hi: return True if addr < lo: return False return False # Gather all RW private/mapped regions and cache buffers scan_regions = [] for r in md.memory_info.infos: st, ty, pr = _ei(r.State), _ei(r.Type), _ei(r.Protect) & 0xff if st != 0x1000: continue if ty == 0x1000000: continue if pr not in (0x04, 0x40): continue scan_regions.append((r.BaseAddress, r.RegionSize)) total_bytes = sum(s for _, s in scan_regions) print(f"scanning {len(scan_regions)} regions ({total_bytes / (1024 * 1024):.1f} MB)") region_bufs = [] # (base, buf) physobj_addrs = [] for base, size in scan_regions: try: reader.move(base) buf = reader.read(size) except Exception: continue if not buf: continue region_bufs.append((base, buf)) end = (len(buf) // 4) * 4 for off in range(0, end, 4): v = struct.unpack_from(" 0x10000000: continue if is_image(vv): field_off = off - back vtable_hits[vv] += 1 vt_off_hits[(vv, field_off)] += 1 field_offsets_per_vtable[vv][field_off] += 1 if len(examples[(vv, field_off)]) < 3: examples[(vv, field_off)].append((hit_va, v)) found = True break if not found: no_vtable += 1 print(f"raw owner-ptr hits: {raw_hits}") print(f"self-vtable slot skipped: {self_skipped}") print(f"hits with no preceding vtable in lookback: {no_vtable}") print() print(f"=== Top owner vtables (regardless of offset) ===") for vt, cnt in vtable_hits.most_common(20): owner = mod_of(vt) or "?" top_offs = field_offsets_per_vtable[vt].most_common(4) offs_str = " ".join(f"+0x{o:x}={c}" for o, c in top_offs) print(f" vt=0x{vt:08x} hits={cnt:<6} ({owner}) {offs_str}") print() print(f"=== Top (owner_vtable, field_offset) pairs ===") for (vt, off), cnt in vt_off_hits.most_common(30): owner = mod_of(vt) or "?" ex = examples[(vt, off)][0] print(f" vt=0x{vt:08x} +0x{off:03x} hits={cnt:<6} ({owner}) e.g. owner@0x{(ex[0] - off):08x} ptr@0x{ex[0]:08x} -> physobj@0x{ex[1]:08x}") if __name__ == "__main__": main()