leakhunt/tools/inspect_regions.py
acbot 57b5e43d0e Initial commit — leak-hunt project complete
Five bugs identified and patched in retail Asheron's Call client:
- v3b: palette refcount over-increment (3-byte NOP at two sites)
- v5: RenderSurface PurgeResource no-op stub (vtable slot 2 thunk)
- v11: two dangling-pointer crash guards (NULL-check + reorder)
- v14: CEnvCell::Destroy ClipPlaneList leak (18-byte JMP to cleanup thunk)
- v22: unpacker stale-pointer SEH guard (whole-function __try/__except)

All five ship in leakfix.dll (117 KB, SHA d282f23c…) which is loaded
by acclient.exe at process start via PE import table patching by
tools/install_leakfix.py.

Controlled 15-client fleet soak: unpatched control died at 26h with
palette exhaustion; all 14 patched clients survived past that point
and reached ≥5-day uptime.

Residual ~15 MB/h growth traced to d3d9.dll's internal slab allocator
(260KB surface backing buffers retained after Release). See REPORT.md
§10 for the full investigation; conclusion is that it's unfixable from
outside d3d9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:07:58 +02:00

59 lines
1.7 KiB
Python

"""
inspect_regions.py <dump.dmp> [N]
Print the first 0x80 bytes of N sample 256-512 KB private RW regions.
Useful for eyeballing what the leaked allocations contain.
"""
import sys, os
from minidump.minidumpfile import MinidumpFile
def _enum_int(v):
if v is None: return 0
if hasattr(v, 'value'): return int(v.value)
return int(v)
def hexdump(buf, base):
lines = []
for i in range(0, len(buf), 16):
chunk = buf[i:i+16]
hexs = " ".join(f"{b:02x}" for b in chunk)
ascii = "".join(chr(b) if 32 <= b < 127 else "." for b in chunk)
lines.append(f" {base+i:08x} {hexs:<47} {ascii}")
return "\n".join(lines)
def main():
dump = sys.argv[1]
n = int(sys.argv[2]) if len(sys.argv) > 2 else 12
md = MinidumpFile.parse(dump)
reader = md.get_reader().get_buffered_reader()
cands = []
for r in md.memory_info.infos:
st = _enum_int(r.State)
ty = _enum_int(r.Type)
pr = _enum_int(r.Protect) & 0xFF
sz = r.RegionSize
if st == 0x1000 and ty == 0x20000 and pr in (0x04, 0x40) \
and 256*1024 <= sz < 512*1024:
cands.append((r.BaseAddress, sz))
print(f"=== {os.path.basename(dump)} : {len(cands)} candidate 256-512KB regions ===")
# Sample evenly across all candidates
step = max(1, len(cands) // n)
samples = cands[::step][:n]
for base, size in samples:
print(f"\n--- region 0x{base:08x} size={size} ({size/1024:.1f} KB) ---")
try:
reader.move(base)
buf = reader.read(0x80)
except Exception as e:
print(f" read failed: {e}"); continue
print(hexdump(buf, base))
if __name__ == "__main__":
main()