"""patch_v15_positionhash_audit.py READ-ONLY audit. Does NOT modify the target process. Goal: confirm that the dominant Position-leak path is through CBaseQualities::_posStatsTable (PackableHashTable), and identify which weenie types host the most retained Positions. Method: 1. Scan live private memory for Position vt 0x00797910 instances. 2. For each, walk back through nearby heap headers to find the containing PositionPropertyValue (vt write at offset -0x08 + m_cRef at -0x04). PositionPropertyValue is the per-property ref-counted holder that lives inside the hash table. 3. From the PositionPropertyValue, look for back-pointers into a PackableHashTable node, then up to the owning CBaseQualities. 4. Print: count per (weenie-vt, dominant property key). Caveats: heap-layout heuristics are fragile. Many Positions are stack-locals, not heap-allocated — those should be filtered out by checking the alignment-stride of the containing region. Use this BEFORE designing any v14-style ctor trace, because it narrows the search: if 95% of leaked Positions are inside one weenie type's quality table, the patch target is that weenie's unload path, not Position itself. """ import ctypes import ctypes.wintypes as wt import struct import sys from collections import Counter POSITION_VT = 0x00797910 # PositionPropertyValue vtable EoR address — DERIVE before use. # 2013 was at &PositionPropertyValue::vftable near 0x00796928. # In EoR the address is near 0x00797928 (placeholder; verify). POSITION_PROP_VAL_VT = 0x00797928 # PLACEHOLDER — verify PROCESS_VM_READ = 0x10 PROCESS_QUERY_INFORMATION = 0x400 class MBI(ctypes.Structure): _fields_ = [('BaseAddress', ctypes.c_void_p), ('AllocationBase', ctypes.c_void_p), ('AllocationProtect', wt.DWORD), ('PartitionId', wt.WORD), ('RegionSize', ctypes.c_size_t), ('State', wt.DWORD), ('Protect', wt.DWORD), ('Type', wt.DWORD)] k = ctypes.windll.kernel32 k.OpenProcess.argtypes = [wt.DWORD, wt.BOOL, wt.DWORD] k.OpenProcess.restype = wt.HANDLE k.CloseHandle.argtypes = [wt.HANDLE] k.CloseHandle.restype = wt.BOOL k.ReadProcessMemory.argtypes = [wt.HANDLE, wt.LPCVOID, wt.LPVOID, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)] k.ReadProcessMemory.restype = wt.BOOL k.VirtualQueryEx.argtypes = [wt.HANDLE, ctypes.c_void_p, ctypes.POINTER(MBI), ctypes.c_size_t] k.VirtualQueryEx.restype = ctypes.c_size_t def audit(pid): h = k.OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid) if not h: print(f"OpenProcess failed (err={ctypes.get_last_error()})") return 1 position_addrs = [] mbi = MBI() addr = 0 while k.VirtualQueryEx(h, addr, ctypes.byref(mbi), ctypes.sizeof(mbi)): pr = mbi.Protect & 0xff if (mbi.State == 0x1000 and mbi.Type == 0x20000 and pr in (0x04, 0x40)): buf = (ctypes.c_ubyte * mbi.RegionSize)() sz = ctypes.c_size_t(0) if k.ReadProcessMemory(h, mbi.BaseAddress, buf, mbi.RegionSize, ctypes.byref(sz)): data = bytes(buf[:sz.value]) end = (len(data) // 4) * 4 for off in range(0, end, 4): if struct.unpack_from("= 0x80000000: break print(f"Total Position instances found: {len(position_addrs)}") # Stage 2: classify by containing region size + density to # separate stack/embedded from heap allocations region_density = Counter() for addr_, base, size in position_addrs: region_density[(base, size)] += 1 print("\nDensity histogram (region_size_kb, hits_per_region, " "count):") bucket = Counter() for (base, size), n in region_density.items(): kb = size // 1024 bucket[(kb, n)] += 1 for (kb, n), c in sorted(bucket.items())[:30]: if c > 1: print(f" region={kb:>5}KB hits={n:>4} count={c:>4}") # Stage 3: for first 200 Position hits, look 8 bytes backwards # for PositionPropertyValue vtable (PPV layout: [vt][m_cRef][position]) ppv_hits = 0 for i, (paddr, base, size) in enumerate(position_addrs[:1000]): ppv_vt_addr = paddr - 8 if ppv_vt_addr < base: continue # Read 4 bytes buf4 = (ctypes.c_ubyte * 4)() sz4 = ctypes.c_size_t(0) if not k.ReadProcessMemory(h, ppv_vt_addr, buf4, 4, ctypes.byref(sz4)): continue vt = struct.unpack("500), the leak is in the " "property-value layer.") print("If low, the leak is in raw Position heap allocations " "(check PositionPack / position_queue paths).") k.CloseHandle(h) return 0 if __name__ == "__main__": sys.exit(audit(int(sys.argv[1])))