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>
102 lines
3.9 KiB
Python
102 lines
3.9 KiB
Python
"""probe_260k_allocation_structure.py <pid>
|
|
For each 260KB private RW region, dump VirtualQuery details (AllocationBase,
|
|
Protect, AllocationProtect, RegionSize) plus VirtualQuery for the pages
|
|
immediately AFTER the region — to detect if it's inside a larger reservation."""
|
|
import ctypes, ctypes.wintypes as wt, sys, struct
|
|
from collections import Counter
|
|
|
|
k = ctypes.windll.kernel32
|
|
k.OpenProcess.argtypes = [wt.DWORD, wt.BOOL, wt.DWORD]; k.OpenProcess.restype = wt.HANDLE
|
|
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 vq(h, addr):
|
|
mbi = MBI()
|
|
if k.VirtualQueryEx(h, addr, ctypes.byref(mbi), ctypes.sizeof(mbi)):
|
|
return mbi
|
|
return None
|
|
|
|
def prot_name(p):
|
|
names = {0x01: "NA", 0x02: "RO", 0x04: "RW", 0x08: "WC", 0x10: "EX",
|
|
0x20: "ER", 0x40: "ERW", 0x80: "ERWC"}
|
|
return names.get(p & 0xFF, f"0x{p:x}")
|
|
|
|
pid = int(sys.argv[1])
|
|
h = k.OpenProcess(0x410, False, pid)
|
|
if not h: print("OpenProcess fail"); sys.exit(2)
|
|
|
|
candidates = []
|
|
addr = 0
|
|
while True:
|
|
mbi = vq(h, addr)
|
|
if not mbi: break
|
|
base = mbi.BaseAddress or 0
|
|
if mbi.State == 0x1000 and mbi.RegionSize == 266240 and (mbi.Type & 0x20000):
|
|
if (mbi.Protect & 0xFF) in (0x04, 0x40):
|
|
candidates.append(mbi.BaseAddress)
|
|
next_addr = base + mbi.RegionSize
|
|
if next_addr <= addr: break
|
|
addr = next_addr
|
|
if addr >= 0x80000000: break
|
|
|
|
print(f"Found {len(candidates)} candidate 260KB regions")
|
|
|
|
# Statistics: base == alloc_base?
|
|
self_alloc = 0
|
|
in_larger = 0
|
|
neighbor_stats = Counter()
|
|
for b in candidates:
|
|
mbi = vq(h, b)
|
|
if not mbi: continue
|
|
base = mbi.BaseAddress
|
|
ab = mbi.AllocationBase
|
|
if base == ab:
|
|
self_alloc += 1
|
|
else:
|
|
in_larger += 1
|
|
# Check neighbor immediately after the 260KB region
|
|
after_addr = base + 266240
|
|
nbr = vq(h, after_addr)
|
|
if nbr:
|
|
same_alloc = (nbr.AllocationBase == ab)
|
|
nbr_state = "COMMIT" if nbr.State == 0x1000 else ("RESERVE" if nbr.State == 0x2000 else "FREE")
|
|
key = f"same_alloc={same_alloc} state={nbr_state}"
|
|
neighbor_stats[key] += 1
|
|
|
|
print(f"\nself_alloc (base == AllocationBase): {self_alloc}")
|
|
print(f"in_larger (base != AllocationBase): {in_larger}")
|
|
print(f"\nNeighbor (page immediately after 260KB region):")
|
|
for key, n in sorted(neighbor_stats.items(), key=lambda x: -x[1]):
|
|
print(f" {key}: {n}")
|
|
|
|
# Dump first 8 in detail
|
|
print(f"\nDetailed dump of first 8 regions:")
|
|
for b in candidates[:8]:
|
|
mbi = vq(h, b)
|
|
if not mbi: continue
|
|
print(f"\n Region @0x{b:08x}:")
|
|
print(f" BaseAddress = 0x{mbi.BaseAddress:08x}")
|
|
print(f" AllocationBase = 0x{mbi.AllocationBase:08x} delta={(mbi.BaseAddress - mbi.AllocationBase):d}")
|
|
print(f" RegionSize = {mbi.RegionSize}")
|
|
print(f" Protect = {prot_name(mbi.Protect)}")
|
|
print(f" AllocProtect = {prot_name(mbi.AllocationProtect)}")
|
|
print(f" State = 0x{mbi.State:x}")
|
|
print(f" Type = 0x{mbi.Type:x}")
|
|
# Look at AllocationBase's full size
|
|
if mbi.BaseAddress != mbi.AllocationBase:
|
|
ab_mbi = vq(h, mbi.AllocationBase)
|
|
if ab_mbi:
|
|
print(f" --- AllocationBase region: ---")
|
|
print(f" base=0x{ab_mbi.BaseAddress:08x} size={ab_mbi.RegionSize} prot={prot_name(ab_mbi.Protect)} type=0x{ab_mbi.Type:x}")
|
|
# Look at neighbor after region
|
|
nbr = vq(h, b + 266240)
|
|
if nbr:
|
|
same = (nbr.AllocationBase == mbi.AllocationBase)
|
|
print(f" --- Neighbor after: 0x{nbr.BaseAddress:08x} size={nbr.RegionSize} prot={prot_name(nbr.Protect)} state=0x{nbr.State:x} same_alloc={same}")
|
|
|
|
k.CloseHandle(h)
|