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>
This commit is contained in:
commit
57b5e43d0e
199 changed files with 1648333 additions and 0 deletions
119
tools/check_exe_pdb.py
Normal file
119
tools/check_exe_pdb.py
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
"""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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue