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

View file

@ -0,0 +1,66 @@
"""
histogram_eor_alloc_sizes.py
Decompile every EoR operator_new caller, extract the size constant, histogram.
"""
import re, urllib.request
from collections import Counter
from concurrent.futures import ThreadPoolExecutor, as_completed
EOR = "http://192.168.1.98:8081"
def http(path, **p):
qs = "&".join(f"{k}={v}" for k, v in p.items())
url = f"{EOR}{path}?{qs}" if qs else f"{EOR}{path}"
with urllib.request.urlopen(url, timeout=60) as r:
return r.read().decode("utf-8", errors="replace")
# Get all xrefs to operator_new
all_refs = []; off = 0
while True:
raw = http("/xrefs_to", address="0x005df0f5", offset=off, limit=500)
batch = [m.group(2) for m in re.finditer(r"From ([0-9a-f]+) in (\S+)", raw)]
if not batch: break
all_refs.extend(batch)
if len(batch) < 500: break
off += 500
# Dedup owner names
owners = sorted(set(all_refs))
addrs = [int(m.group(1), 16) for n in owners for m in [re.match(r"FUN_([0-9a-f]+)", n)] if m]
print(f"{len(addrs)} unique owners to scan")
def scan(addr):
try:
body = http("/decompile_function_by_address", address=f"0x{addr:08x}")
except Exception:
return (addr, [])
# Extract operator_new size args
sizes = []
for m in re.finditer(r"FUN_005df0f5\((0x[0-9a-fA-F]+|\d+)\)", body):
v = m.group(1)
sizes.append(int(v, 0))
for m in re.finditer(r"thunk_FUN_005df0f5\((0x[0-9a-fA-F]+|\d+)\)", body):
v = m.group(1)
sizes.append(int(v, 0))
return (addr, sizes)
results = {}
size_hist = Counter()
with ThreadPoolExecutor(max_workers=24) as ex:
futures = [ex.submit(scan, a) for a in addrs]
for fut in as_completed(futures):
addr, sizes = fut.result()
results[addr] = sizes
for s in sizes:
size_hist[s] += 1
print(f"\n=== size histogram of operator_new calls ===")
for sz, cnt in sorted(size_hist.items(), key=lambda x: -x[1])[:40]:
print(f" 0x{sz:08x} ({sz:>10}) : {cnt}")
print(f"\n=== sizes near 0x100-0x200 (RenderSurface candidates) ===")
for sz in sorted(size_hist):
if 0xf0 <= sz <= 0x250:
# Find addrs that call operator_new with this size
callers = [a for a, sizes in results.items() if sz in sizes]
print(f" 0x{sz:x} ({sz}) -> {len(callers)} callers: {[hex(a) for a in callers[:8]]}")