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>
119 lines
4.6 KiB
Python
119 lines
4.6 KiB
Python
"""Check an .exe's CodeView debug info to see what PDB GUID + age it
|
|
expects. Used to verify whether a candidate acclient.exe matches our
|
|
acclient.pdb without running the binary.
|
|
|
|
Usage:
|
|
py tools/pdb-extract/check_exe_pdb.py <path-to-exe>
|
|
"""
|
|
|
|
import struct
|
|
import sys
|
|
import uuid
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print("usage: check_exe_pdb.py <path-to-exe>")
|
|
sys.exit(1)
|
|
|
|
with open(sys.argv[1], "rb") as f:
|
|
data = f.read()
|
|
|
|
# DOS header -> e_lfanew @ offset 0x3C points to PE header
|
|
pe_off = struct.unpack_from("<I", data, 0x3C)[0]
|
|
assert data[pe_off:pe_off + 4] == b"PE\x00\x00", "not a PE file"
|
|
|
|
# COFF header
|
|
machine = struct.unpack_from("<H", data, pe_off + 4)[0]
|
|
n_sections = struct.unpack_from("<H", data, pe_off + 6)[0]
|
|
timestamp = struct.unpack_from("<I", data, pe_off + 8)[0]
|
|
opt_size = struct.unpack_from("<H", data, pe_off + 20)[0]
|
|
|
|
print(f"machine = 0x{machine:04x}")
|
|
print(f"timestamp = 0x{timestamp:08x} ({timestamp})")
|
|
|
|
import datetime
|
|
ts = datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc)
|
|
print(f" -> linker UTC: {ts.isoformat()}")
|
|
|
|
# Optional header — magic indicates 32 vs 64 bit
|
|
opt_off = pe_off + 24
|
|
magic = struct.unpack_from("<H", data, opt_off)[0]
|
|
is_pe32_plus = (magic == 0x20B)
|
|
print(f"opt magic = 0x{magic:04x} ({'PE32+' if is_pe32_plus else 'PE32'})")
|
|
|
|
# Data directories: PE32 has them at opt_off + 96; PE32+ at opt_off + 112
|
|
dd_off = opt_off + (112 if is_pe32_plus else 96)
|
|
# Debug directory is data dir [6]
|
|
debug_va = struct.unpack_from("<I", data, dd_off + 6 * 8)[0]
|
|
debug_size = struct.unpack_from("<I", data, dd_off + 6 * 8 + 4)[0]
|
|
print(f"debug dir = VA=0x{debug_va:08x} size={debug_size}")
|
|
|
|
# We need to map the VA back to a file offset via section headers
|
|
sec_off = opt_off + opt_size
|
|
sections = []
|
|
for i in range(n_sections):
|
|
s = sec_off + i * 40
|
|
name = data[s:s + 8].rstrip(b"\x00").decode("ascii", errors="replace")
|
|
vsize = struct.unpack_from("<I", data, s + 8)[0]
|
|
vaddr = struct.unpack_from("<I", data, s + 12)[0]
|
|
rsize = struct.unpack_from("<I", data, s + 16)[0]
|
|
roff = struct.unpack_from("<I", data, s + 20)[0]
|
|
sections.append((name, vaddr, vsize, roff, rsize))
|
|
|
|
def va_to_file(va):
|
|
for (name, vaddr, vsize, roff, rsize) in sections:
|
|
if vaddr <= va < vaddr + vsize:
|
|
return roff + (va - vaddr)
|
|
return None
|
|
|
|
debug_off = va_to_file(debug_va)
|
|
if debug_off is None:
|
|
print("debug directory VA does not map into any section")
|
|
return
|
|
|
|
# Each debug directory entry is 28 bytes
|
|
n_entries = debug_size // 28
|
|
print(f"# debug entries = {n_entries}")
|
|
|
|
for i in range(n_entries):
|
|
e = debug_off + i * 28
|
|
characteristics = struct.unpack_from("<I", data, e)[0]
|
|
ts_e = struct.unpack_from("<I", data, e + 4)[0]
|
|
major = struct.unpack_from("<H", data, e + 8)[0]
|
|
minor = struct.unpack_from("<H", data, e + 10)[0]
|
|
type_e = struct.unpack_from("<I", data, e + 12)[0]
|
|
sz = struct.unpack_from("<I", data, e + 16)[0]
|
|
rva = struct.unpack_from("<I", data, e + 20)[0]
|
|
ptr = struct.unpack_from("<I", data, e + 24)[0]
|
|
|
|
type_name = {2: "CODEVIEW", 4: "MISC", 12: "VC_FEATURE", 13: "POGO", 16: "REPRO"}.get(type_e, f"type_{type_e}")
|
|
print(f" entry {i}: type={type_name} sz={sz} fileOff=0x{ptr:08x}")
|
|
|
|
if type_e == 2 and sz >= 24:
|
|
cv = data[ptr:ptr + sz]
|
|
sig = cv[:4]
|
|
print(f" cv signature = {sig!r}")
|
|
if sig == b"RSDS":
|
|
guid_bytes = cv[4:20]
|
|
age = struct.unpack_from("<I", cv, 20)[0]
|
|
pdb_name = cv[24:].rstrip(b"\x00").decode("utf-8", errors="replace")
|
|
pdb_guid = uuid.UUID(bytes_le=guid_bytes)
|
|
print(f" GUID = {{{pdb_guid}}}")
|
|
print(f" age = {age}")
|
|
print(f" PDB filename = {pdb_name}")
|
|
|
|
expected_guid = uuid.UUID("9e847e2f-777c-4bd9-886c-22256bb87f32")
|
|
expected_age = 1
|
|
if pdb_guid == expected_guid and age == expected_age:
|
|
print()
|
|
print("=== MATCH: this exe pairs with our acclient.pdb ===")
|
|
else:
|
|
print()
|
|
print("=== MISMATCH ===")
|
|
print(f" expected GUID = {{{expected_guid}}}")
|
|
print(f" expected age = {expected_age}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|