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:
acbot 2026-05-23 21:05:17 +02:00
commit 57b5e43d0e
199 changed files with 1648333 additions and 0 deletions

192
tools/clone_dump.py Normal file
View file

@ -0,0 +1,192 @@
"""clone_dump.py <pid> <out.dmp>
Take a non-disruptive full memory dump of a live process using
process reflection (PssCaptureSnapshot) the same mechanism procdump
uses with -r 1 -ma. The target is only paused for ~1ms while the
COW snapshot is created; the dump itself runs against the snapshot,
not the live process.
This avoids the multi-second pause that MiniDumpWriteDump on a live
PID would cause (and which disconnects AC clients from Coldeve).
"""
import argparse
import ctypes
import ctypes.wintypes as wt
import sys
import os
# PSS flags
PSS_CAPTURE_VA_CLONE = 0x00000001
PSS_CAPTURE_HANDLES = 0x00000004
PSS_CAPTURE_HANDLE_NAME_INFORMATION = 0x00000008
PSS_CAPTURE_HANDLE_BASIC_INFORMATION= 0x00000010
PSS_CAPTURE_HANDLE_TYPE_SPECIFIC_INFORMATION = 0x00000020
PSS_CAPTURE_HANDLE_TRACE = 0x00000040
PSS_CAPTURE_THREADS = 0x00000080
PSS_CAPTURE_THREAD_CONTEXT = 0x00000100
PSS_CAPTURE_THREAD_CONTEXT_EXTENDED = 0x00000200
PSS_CAPTURE_VA_SPACE = 0x00000800
PSS_CAPTURE_VA_SPACE_SECTION_INFORMATION = 0x00001000
PSS_CREATE_RELEASE_SECTION = 0x80000000
PSS_CREATE_FORCE_BREAKAWAY = 0x40000000
PSS_CREATE_USE_VM_ALLOCATIONS = 0x20000000
# Process access
PROCESS_ALL_ACCESS = 0x1F0FFF
# MiniDumpType
MINI_DUMP_WITH_FULL_MEMORY = 0x00000002
MINI_DUMP_WITH_HANDLE_DATA = 0x00000004
MINI_DUMP_WITH_UNLOADED_MODULES = 0x00000020
MINI_DUMP_WITH_FULL_MEMORY_INFO = 0x00000800
MINI_DUMP_WITH_THREAD_INFO = 0x00001000
MINI_DUMP_WITH_TOKEN_INFORMATION = 0x00040000
DUMP_TYPE = (
MINI_DUMP_WITH_FULL_MEMORY
| MINI_DUMP_WITH_HANDLE_DATA
| MINI_DUMP_WITH_UNLOADED_MODULES
| MINI_DUMP_WITH_FULL_MEMORY_INFO
| MINI_DUMP_WITH_THREAD_INFO
)
k32 = ctypes.WinDLL('kernel32', use_last_error=True)
dbghelp = ctypes.WinDLL('dbghelp', use_last_error=True)
OpenProcess = k32.OpenProcess
OpenProcess.argtypes = [wt.DWORD, wt.BOOL, wt.DWORD]
OpenProcess.restype = wt.HANDLE
CloseHandle = k32.CloseHandle
CloseHandle.argtypes = [wt.HANDLE]
CloseHandle.restype = wt.BOOL
PssCaptureSnapshot = k32.PssCaptureSnapshot
PssCaptureSnapshot.argtypes = [wt.HANDLE, wt.DWORD, wt.DWORD, ctypes.POINTER(wt.HANDLE)]
PssCaptureSnapshot.restype = wt.DWORD
PssFreeSnapshot = k32.PssFreeSnapshot
PssFreeSnapshot.argtypes = [wt.HANDLE, wt.HANDLE]
PssFreeSnapshot.restype = wt.DWORD
CreateFileW = k32.CreateFileW
CreateFileW.argtypes = [wt.LPCWSTR, wt.DWORD, wt.DWORD, ctypes.c_void_p,
wt.DWORD, wt.DWORD, wt.HANDLE]
CreateFileW.restype = wt.HANDLE
MiniDumpWriteDump = dbghelp.MiniDumpWriteDump
MiniDumpWriteDump.argtypes = [wt.HANDLE, wt.DWORD, wt.HANDLE,
wt.DWORD, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p]
MiniDumpWriteDump.restype = wt.BOOL
# MINIDUMP_CALLBACK_INFORMATION + IsProcessSnapshotCallback support
# to tell dbghelp the hProcess is actually a snapshot handle.
class MINIDUMP_CALLBACK_INFORMATION(ctypes.Structure):
_fields_ = [
("CallbackRoutine", ctypes.c_void_p),
("CallbackParam", ctypes.c_void_p),
]
# Callback type: BOOL CALLBACK MiniDumpCallback(PVOID CallbackParam, const PMINIDUMP_CALLBACK_INPUT, PMINIDUMP_CALLBACK_OUTPUT)
MiniDumpCallback_T = ctypes.WINFUNCTYPE(wt.BOOL, ctypes.c_void_p, ctypes.c_void_p, ctypes.c_void_p)
IS_PROCESS_SNAPSHOT_CALLBACK = 16
def _callback(callback_param, input_ptr, output_ptr):
# CallbackType is at offset 4 in MINIDUMP_CALLBACK_INPUT (after ProcessId DWORD)
if not input_ptr:
return True
cb_type = ctypes.cast(input_ptr + 4, ctypes.POINTER(wt.ULONG))[0]
if cb_type == IS_PROCESS_SNAPSHOT_CALLBACK:
# Set ULONG at start of MINIDUMP_CALLBACK_OUTPUT to MiniDumpValidCallback (1)
if output_ptr:
ctypes.cast(output_ptr, ctypes.POINTER(wt.ULONG))[0] = 1
return True
_callback_inst = MiniDumpCallback_T(_callback)
GetCurrentProcess = k32.GetCurrentProcess
GetCurrentProcess.argtypes = []
GetCurrentProcess.restype = wt.HANDLE
GENERIC_WRITE = 0x40000000
CREATE_ALWAYS = 2
FILE_ATTRIBUTE_NORMAL = 0x80
INVALID_HANDLE_VALUE = wt.HANDLE(-1).value
def main():
ap = argparse.ArgumentParser()
ap.add_argument("pid", type=int)
ap.add_argument("out", help="output dump path (.dmp)")
args = ap.parse_args()
out_path = os.path.abspath(args.out)
h_proc = OpenProcess(PROCESS_ALL_ACCESS, False, args.pid)
if not h_proc:
err = ctypes.get_last_error()
print(f"OpenProcess({args.pid}) failed err={err}")
sys.exit(2)
print(f"PID {args.pid}: opened, capturing COW snapshot...")
capture_flags = (
PSS_CAPTURE_VA_CLONE
| PSS_CAPTURE_HANDLES
| PSS_CAPTURE_HANDLE_NAME_INFORMATION
| PSS_CAPTURE_HANDLE_BASIC_INFORMATION
| PSS_CAPTURE_HANDLE_TYPE_SPECIFIC_INFORMATION
| PSS_CAPTURE_HANDLE_TRACE
| PSS_CAPTURE_THREADS
| PSS_CAPTURE_THREAD_CONTEXT
| PSS_CAPTURE_THREAD_CONTEXT_EXTENDED
| PSS_CAPTURE_VA_SPACE
| PSS_CAPTURE_VA_SPACE_SECTION_INFORMATION
)
h_snap = wt.HANDLE()
# ContextFlags param: 0x0010001F = CONTEXT_FULL | CONTEXT_i386
rc = PssCaptureSnapshot(h_proc, capture_flags, 0x0010001F, ctypes.byref(h_snap))
if rc != 0:
print(f"PssCaptureSnapshot failed rc={rc}")
CloseHandle(h_proc)
sys.exit(3)
print(f" snapshot handle 0x{h_snap.value:x}")
h_file = CreateFileW(out_path, GENERIC_WRITE, 0, None,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, None)
if h_file == INVALID_HANDLE_VALUE or h_file is None:
err = ctypes.get_last_error()
print(f"CreateFile {out_path} failed err={err}")
PssFreeSnapshot(GetCurrentProcess(), h_snap)
CloseHandle(h_proc)
sys.exit(4)
print(f" writing dump to {out_path}...")
cb_info = MINIDUMP_CALLBACK_INFORMATION()
cb_info.CallbackRoutine = ctypes.cast(_callback_inst, ctypes.c_void_p).value
cb_info.CallbackParam = None
ok = MiniDumpWriteDump(h_snap, args.pid, h_file, DUMP_TYPE, None, None, ctypes.byref(cb_info))
if not ok:
err = ctypes.get_last_error()
print(f"MiniDumpWriteDump failed err={err}")
CloseHandle(h_file)
PssFreeSnapshot(GetCurrentProcess(), h_snap)
CloseHandle(h_proc)
sys.exit(5)
CloseHandle(h_file)
PssFreeSnapshot(GetCurrentProcess(), h_snap)
CloseHandle(h_proc)
sz = os.path.getsize(out_path)
print(f" OK: {sz/1e6:.1f} MB written")
if __name__ == "__main__":
main()