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
102
tools/probe_260k_allocation_structure.py
Normal file
102
tools/probe_260k_allocation_structure.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
"""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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue