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
192
tools/clone_dump.py
Normal file
192
tools/clone_dump.py
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue