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>
65 lines
2.4 KiB
Python
65 lines
2.4 KiB
Python
"""check_acclient_imports.py
|
|
Read acclient.exe imports and report whether leakfix.dll is already loaded."""
|
|
import struct, sys, os
|
|
|
|
EXE = r"C:\Turbine\Asheron's Call\acclient.exe"
|
|
if len(sys.argv) > 1: EXE = sys.argv[1]
|
|
|
|
with open(EXE, 'rb') as f:
|
|
data = f.read()
|
|
print(f"Loaded {len(data):,} bytes from {EXE}")
|
|
|
|
pe_off = struct.unpack_from('<I', data, 0x3C)[0]
|
|
print(f"PE header at 0x{pe_off:x}, signature: {data[pe_off:pe_off+4]}")
|
|
|
|
num_sections = struct.unpack_from('<H', data, pe_off + 4 + 2)[0]
|
|
opt_header_size = struct.unpack_from('<H', data, pe_off + 4 + 16)[0]
|
|
opt_off = pe_off + 4 + 20
|
|
magic = struct.unpack_from('<H', data, opt_off)[0]
|
|
print(f"sections={num_sections}, opt_hdr={opt_header_size}, magic=0x{magic:x}")
|
|
|
|
# Read sections to enable RVA→file translation
|
|
sect_off = opt_off + opt_header_size
|
|
sections = []
|
|
for i in range(num_sections):
|
|
so = sect_off + i*40
|
|
name = data[so:so+8].rstrip(b'\0').decode(errors='replace')
|
|
vsize = struct.unpack_from('<I', data, so+8)[0]
|
|
vaddr = struct.unpack_from('<I', data, so+12)[0]
|
|
rsize = struct.unpack_from('<I', data, so+16)[0]
|
|
rawoff = struct.unpack_from('<I', data, so+20)[0]
|
|
chars = struct.unpack_from('<I', data, so+36)[0]
|
|
sections.append((name, vaddr, vsize, rawoff, rsize, chars))
|
|
print(f" [{i}] {name:8s} vaddr=0x{vaddr:08x} vsize=0x{vsize:08x} raw=0x{rawoff:08x} rsize=0x{rsize:08x} chars=0x{chars:08x}")
|
|
|
|
def rva_to_off(rva):
|
|
for name, vaddr, vsize, rawoff, rsize, chars in sections:
|
|
if vaddr <= rva < vaddr + vsize:
|
|
return rawoff + (rva - vaddr)
|
|
return None
|
|
|
|
# DataDirectory[1] = Import Table
|
|
dd_off = opt_off + 96
|
|
import_rva = struct.unpack_from('<I', data, dd_off + 8)[0]
|
|
import_sz = struct.unpack_from('<I', data, dd_off + 12)[0]
|
|
print(f"\nImport directory: RVA=0x{import_rva:x} size={import_sz}")
|
|
import_foff = rva_to_off(import_rva)
|
|
print(f"Import directory file offset: 0x{import_foff:x}")
|
|
|
|
print("\nImports:")
|
|
off = import_foff
|
|
dlls = []
|
|
while True:
|
|
name_rva = struct.unpack_from('<I', data, off + 12)[0]
|
|
if name_rva == 0: break
|
|
name_foff = rva_to_off(name_rva)
|
|
name_end = data.index(b'\0', name_foff)
|
|
dll_name = data[name_foff:name_end].decode(errors='replace')
|
|
dlls.append((dll_name, off))
|
|
off += 20
|
|
|
|
for name, descriptor_off in dlls:
|
|
print(f" {name} (descriptor @ file 0x{descriptor_off:x})")
|
|
|
|
print(f"\nTotal: {len(dlls)} imports")
|
|
print(f"leakfix.dll already in imports? {any('leakfix' in n.lower() for n, _ in dlls)}")
|