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>
98 lines
3.1 KiB
Python
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()
|