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>
53 lines
1.6 KiB
Python
53 lines
1.6 KiB
Python
"""inspect_vtable.py <dump.dmp> <vtable_va> [num_slots]
|
|
Read N dwords starting at vtable_va. For each, mark whether it is in
|
|
code memory (image, executable). A valid vtable is a row of >= 4
|
|
contiguous code pointers.
|
|
"""
|
|
import struct, sys
|
|
from minidump.minidumpfile import MinidumpFile
|
|
|
|
|
|
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])
|
|
vt = int(sys.argv[2], 16)
|
|
n = int(sys.argv[3]) if len(sys.argv) > 3 else 32
|
|
|
|
# Module map
|
|
mods = []
|
|
for m in md.modules.modules:
|
|
mods.append((m.baseaddress, m.size, m.name.split("\\")[-1]))
|
|
def mod_of(a):
|
|
for b, s, nm in mods:
|
|
if b <= a < b + s: return nm
|
|
return None
|
|
|
|
# Executable image-region cache
|
|
exec_ranges = []
|
|
for r in md.memory_info.infos:
|
|
st, ty, pr = _ei(r.State), _ei(r.Type), _ei(r.Protect) & 0xff
|
|
if st == 0x1000 and ty == 0x1000000 and pr in (0x20, 0x80):
|
|
exec_ranges.append((r.BaseAddress, r.BaseAddress + r.RegionSize))
|
|
def is_exec(a):
|
|
for lo, hi in exec_ranges:
|
|
if lo <= a < hi: return True
|
|
return False
|
|
|
|
rdr = md.get_reader().get_buffered_reader()
|
|
rdr.move(vt)
|
|
buf = rdr.read(n * 4)
|
|
print(f"vtable @ 0x{vt:08x} ({mod_of(vt) or '?'}):")
|
|
for i in range(n):
|
|
v = struct.unpack_from("<I", buf, i*4)[0]
|
|
owner = mod_of(v) or "?"
|
|
exe = "CODE" if is_exec(v) else " "
|
|
print(f" [{i:2d}] +0x{i*4:02x} 0x{v:08x} {exe} ({owner})")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|