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
103
tools/build_patched_binary.py
Normal file
103
tools/build_patched_binary.py
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
"""build_patched_binary.py
|
||||
|
||||
Build acclient.eor.patched.exe by applying the v3b byte patches to a
|
||||
backed-up copy of the original EoR acclient.exe.
|
||||
|
||||
Patches (file offsets = VA - 0x400 since base=0x400000 and .text RVA is +0x400):
|
||||
- VA 0x0053effe ff 40 24 -> 90 90 90 ; NOP makeModifiedPalette() over-increment
|
||||
- VA 0x0053f19c ff 46 24 -> 90 90 90 ; NOP makeModifiedPalette(id,sub) over-increment
|
||||
|
||||
For 32-bit PE with base=0x00400000 and .text starting at RVA 0x1000
|
||||
mapped to file offset 0x400 (standard MSVC layout), file_offset =
|
||||
VA - 0x00400000 - 0x1000 + 0x400 = VA - 0x00400C00.
|
||||
"""
|
||||
import hashlib, os, shutil, struct, sys
|
||||
|
||||
|
||||
SRC = r"C:\Turbine\Asheron's Call\acclient.exe"
|
||||
ORIG = r"C:\Turbine\Asheron's Call\acclient.eor.orig.exe"
|
||||
PATCHED = r"C:\Turbine\Asheron's Call\acclient.eor.patched.exe"
|
||||
|
||||
# VA-based patches (we resolve file offsets via PE parsing)
|
||||
PATCHES = [
|
||||
(0x0053effe, bytes([0xff, 0x40, 0x24]), bytes([0x90, 0x90, 0x90])),
|
||||
(0x0053f19c, bytes([0xff, 0x46, 0x24]), bytes([0x90, 0x90, 0x90])),
|
||||
]
|
||||
|
||||
|
||||
def parse_pe_sections(data):
|
||||
"""Return list of (rva_start, rva_size, file_offset) tuples."""
|
||||
e_lfanew = struct.unpack_from("<I", data, 0x3c)[0]
|
||||
sig = data[e_lfanew:e_lfanew+4]
|
||||
assert sig == b"PE\0\0", f"bad PE sig: {sig}"
|
||||
# COFF header at e_lfanew + 4
|
||||
num_sections = struct.unpack_from("<H", data, e_lfanew + 6)[0]
|
||||
size_opt_hdr = struct.unpack_from("<H", data, e_lfanew + 0x14)[0]
|
||||
sections_off = e_lfanew + 0x18 + size_opt_hdr
|
||||
image_base = struct.unpack_from("<I", data, e_lfanew + 0x34)[0] # PE32 ImageBase
|
||||
out = []
|
||||
for i in range(num_sections):
|
||||
sh = sections_off + i * 0x28
|
||||
virtual_size = struct.unpack_from("<I", data, sh + 0x08)[0]
|
||||
virtual_addr = struct.unpack_from("<I", data, sh + 0x0c)[0]
|
||||
raw_size = struct.unpack_from("<I", data, sh + 0x10)[0]
|
||||
raw_off = struct.unpack_from("<I", data, sh + 0x14)[0]
|
||||
name = data[sh:sh+8].rstrip(b"\0").decode("ascii", "replace")
|
||||
out.append((name, image_base + virtual_addr, virtual_size, raw_off, raw_size))
|
||||
return image_base, out
|
||||
|
||||
|
||||
def va_to_file_offset(va, sections):
|
||||
for name, va_start, vsize, raw_off, raw_size in sections:
|
||||
if va_start <= va < va_start + max(vsize, raw_size):
|
||||
return raw_off + (va - va_start)
|
||||
raise ValueError(f"VA 0x{va:08x} not in any section")
|
||||
|
||||
|
||||
def main():
|
||||
if not os.path.exists(SRC):
|
||||
print(f"src not found: {SRC}"); sys.exit(1)
|
||||
|
||||
# Backup
|
||||
if not os.path.exists(ORIG):
|
||||
shutil.copy2(SRC, ORIG)
|
||||
print(f"backup written: {ORIG}")
|
||||
else:
|
||||
print(f"backup already exists: {ORIG}")
|
||||
|
||||
with open(ORIG, "rb") as f:
|
||||
data = bytearray(f.read())
|
||||
|
||||
sha_orig = hashlib.sha256(data).hexdigest()
|
||||
print(f"orig sha256: {sha_orig}")
|
||||
print(f"orig size: {len(data)}")
|
||||
|
||||
image_base, sections = parse_pe_sections(data)
|
||||
print(f"image base: 0x{image_base:08x}")
|
||||
for s in sections[:3]:
|
||||
print(f" section {s[0]:<8} va=0x{s[1]:08x} vsize=0x{s[2]:x} raw=0x{s[3]:08x} rsize=0x{s[4]:x}")
|
||||
|
||||
# Apply patches
|
||||
for va, orig_bytes, patched_bytes in PATCHES:
|
||||
off = va_to_file_offset(va, sections)
|
||||
actual = bytes(data[off:off+len(orig_bytes)])
|
||||
if actual == orig_bytes:
|
||||
data[off:off+len(patched_bytes)] = patched_bytes
|
||||
print(f" patched VA 0x{va:08x} (file off 0x{off:x}): "
|
||||
f"{' '.join(f'{b:02x}' for b in orig_bytes)} -> "
|
||||
f"{' '.join(f'{b:02x}' for b in patched_bytes)}")
|
||||
elif actual == patched_bytes:
|
||||
print(f" VA 0x{va:08x} already patched, skipping")
|
||||
else:
|
||||
print(f" VA 0x{va:08x} UNEXPECTED bytes: {' '.join(f'{b:02x}' for b in actual)}")
|
||||
sys.exit(2)
|
||||
|
||||
with open(PATCHED, "wb") as f:
|
||||
f.write(data)
|
||||
sha_new = hashlib.sha256(bytes(data)).hexdigest()
|
||||
print(f"\npatched sha256: {sha_new}")
|
||||
print(f"written: {PATCHED}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue