leakhunt/tools/dump_pdb_info.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

98 lines
3.1 KiB
Python

"""Dump the PDB info stream so we know exactly which acclient.exe build
matches our PDB GUID. The PDB header points to stream 1 ("PDB Info") which
contains: u32 version, u32 signature(timestamp), u32 age, 16-byte GUID.
Usage:
py tools/pdb-extract/dump_pdb_info.py refs/acclient.pdb
"""
import struct
import sys
import datetime
import uuid
def _ceil_div(a, b):
return (a + b - 1) // b
def main():
if len(sys.argv) < 2:
print("usage: dump_pdb_info.py <path-to-pdb>")
sys.exit(1)
pdb_path = sys.argv[1]
with open(pdb_path, "rb") as f:
data = f.read()
magic = b"Microsoft C/C++ MSF 7.00\r\n\x1aDS\x00\x00\x00"
assert data.startswith(magic), "not an MSF 7.00 PDB"
block_size = struct.unpack_from("<I", data, 0x20)[0]
num_blocks = struct.unpack_from("<I", data, 0x28)[0]
num_dir_bytes = struct.unpack_from("<I", data, 0x2C)[0]
block_map_addr = struct.unpack_from("<I", data, 0x34)[0]
print(f"block_size = {block_size}")
print(f"num_blocks = {num_blocks}")
print(f"num_dir_bytes = {num_dir_bytes}")
print(f"block_map_addr = {block_map_addr}")
def read_page(idx):
return data[idx * block_size : (idx + 1) * block_size]
dir_pages_needed = _ceil_div(num_dir_bytes, block_size)
block_map = read_page(block_map_addr)
dir_page_indices = struct.unpack_from(f"<{dir_pages_needed}I", block_map, 0)
dir_data = bytearray()
for pi in dir_page_indices:
dir_data.extend(read_page(pi))
dir_data = bytes(dir_data)
num_streams = struct.unpack_from("<I", dir_data, 0)[0]
stream_sizes = struct.unpack_from(f"<{num_streams}I", dir_data, 4)
print(f"num_streams = {num_streams}")
offset = 4 + num_streams * 4
streams = []
for sz in stream_sizes:
if sz == 0xFFFFFFFF:
streams.append((0, []))
continue
n_pages = _ceil_div(sz, block_size)
pages = struct.unpack_from(f"<{n_pages}I", dir_data, offset)
offset += n_pages * 4
streams.append((sz, list(pages)))
# Stream 1 = PDB Info Stream
pdb_info_size, pdb_info_pages = streams[1]
print(f"pdb_info_size = {pdb_info_size}")
pdb_info = bytearray()
for pi in pdb_info_pages:
pdb_info.extend(read_page(pi))
pdb_info = bytes(pdb_info[:pdb_info_size])
version = struct.unpack_from("<I", pdb_info, 0)[0]
signature = struct.unpack_from("<I", pdb_info, 4)[0]
age = struct.unpack_from("<I", pdb_info, 8)[0]
guid_bytes = pdb_info[12:28]
pdb_guid = uuid.UUID(bytes_le=guid_bytes)
sig_dt = datetime.datetime.fromtimestamp(signature, tz=datetime.timezone.utc)
print()
print("=== PDB Info Stream ===")
print(f"version = {version}")
print(f"signature = 0x{signature:08x} ({signature})")
print(f" -> linker timestamp UTC: {sig_dt.isoformat()}")
print(f"age = {age}")
print(f"GUID = {{{pdb_guid}}}")
print()
print("This is the GUID + age the matching acclient.exe must reference")
print("in its CodeView entry. Find a binary whose linker timestamp")
print(f"is around {sig_dt.strftime('%Y-%m-%d')}.")
if __name__ == "__main__":
main()