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>
73 lines
2.8 KiB
Python
73 lines
2.8 KiB
Python
"""dump_260k_content.py <pid>
|
|
For a few 260KB regions, dump first 128 bytes + mid + tail to characterize
|
|
whether they're zeroed (free pool), structured (live d3d9 surface), or
|
|
texture data."""
|
|
import ctypes, ctypes.wintypes as wt, sys, struct
|
|
|
|
k = ctypes.windll.kernel32
|
|
k.OpenProcess.argtypes = [wt.DWORD, wt.BOOL, wt.DWORD]; k.OpenProcess.restype = wt.HANDLE
|
|
k.ReadProcessMemory.argtypes = [wt.HANDLE, wt.LPCVOID, wt.LPVOID, ctypes.c_size_t, ctypes.POINTER(ctypes.c_size_t)]
|
|
k.ReadProcessMemory.restype = wt.BOOL
|
|
k.VirtualQueryEx.argtypes = [wt.HANDLE, wt.LPCVOID, ctypes.c_void_p, ctypes.c_size_t]
|
|
k.VirtualQueryEx.restype = ctypes.c_size_t
|
|
|
|
class MBI(ctypes.Structure):
|
|
_fields_ = [("BaseAddress", ctypes.c_void_p), ("AllocationBase", ctypes.c_void_p),
|
|
("AllocationProtect", wt.DWORD), ("RegionSize", ctypes.c_size_t),
|
|
("State", wt.DWORD), ("Protect", wt.DWORD), ("Type", wt.DWORD)]
|
|
|
|
def rd(h, va, n):
|
|
buf = (ctypes.c_ubyte * n)(); sz = ctypes.c_size_t(0)
|
|
if not k.ReadProcessMemory(h, va, buf, n, ctypes.byref(sz)): return None
|
|
return bytes(buf[:sz.value])
|
|
|
|
pid = int(sys.argv[1])
|
|
h = k.OpenProcess(0x410, False, pid)
|
|
if not h: print("OpenProcess fail"); sys.exit(2)
|
|
|
|
# Find 260KB regions
|
|
candidates = []
|
|
mbi = MBI(); addr = 0
|
|
while k.VirtualQueryEx(h, addr, ctypes.byref(mbi), ctypes.sizeof(mbi)):
|
|
base = mbi.BaseAddress or 0
|
|
if mbi.State == 0x1000 and mbi.RegionSize == 266240 and (mbi.Type & 0x20000):
|
|
candidates.append(base)
|
|
next_addr = base + mbi.RegionSize
|
|
if next_addr <= addr: break
|
|
addr = next_addr
|
|
if addr >= 0x80000000: break
|
|
|
|
print(f"260KB regions: {len(candidates)}")
|
|
|
|
# Histogram of first DWORD across ALL of them
|
|
from collections import Counter
|
|
first_dwords = []
|
|
for b in candidates:
|
|
d = rd(h, b, 4)
|
|
if d: first_dwords.append(struct.unpack('<I', d)[0])
|
|
c = Counter(first_dwords)
|
|
print("Top first-DWORD values (offset 0):")
|
|
for v, n in c.most_common(8):
|
|
print(f" 0x{v:08x}: {n} ({n*100/len(first_dwords):.1f}%)")
|
|
|
|
# Sample a few in detail
|
|
import random
|
|
random.seed(42)
|
|
sample_count = 4
|
|
sample = random.sample(candidates, sample_count)
|
|
for b in sample:
|
|
print(f"\n=== Region @0x{b:08x} (260KB) ===")
|
|
for off, label in [(0, "first 64B"), (256, "+0x100"),
|
|
(4096, "+0x1000 (start of pixel data?)"),
|
|
(133120, "midpoint"),
|
|
(262144, "+0x40000 (256KB mark)"),
|
|
(266112, "last 128 bytes")]:
|
|
d = rd(h, b + off, 64 if 'first' in label or 'last' not in label else 128)
|
|
if not d: continue
|
|
# Count zero bytes
|
|
zeros = d.count(0)
|
|
non_zero = len(d) - zeros
|
|
print(f" +0x{off:06x} ({label}, {zeros}/{len(d)} zero): "
|
|
f"{d[:32].hex(' ')}{'...' if len(d)>32 else ''}")
|
|
|
|
k.CloseHandle(h)
|