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>
98 lines
4.3 KiB
Python
98 lines
4.3 KiB
Python
"""find_d3d9_via_iat.py <pid>
|
|
Find d3d9.dll's load address by walking AC's import table.
|
|
Strategy: locate the string "Direct3DCreate9" in AC's image, then find
|
|
the IAT entry that resolves it. The IAT slot holds the runtime address
|
|
of Direct3DCreate9 (= address INSIDE d3d9.dll). VirtualQuery on that
|
|
address gives us d3d9.dll's base + size."""
|
|
import ctypes, ctypes.wintypes as wt, sys, struct
|
|
|
|
PROCESS_VM_READ = 0x10
|
|
PROCESS_QUERY_INFORMATION = 0x400
|
|
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(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, False, pid)
|
|
if not h: print("OpenProcess fail"); sys.exit(2)
|
|
|
|
# AC's image starts at 0x00400000. Read its full size from PE header.
|
|
hdr = rd(h, 0x00400000 + 0x3C, 4)
|
|
if not hdr: print("can't read AC PE header"); sys.exit(2)
|
|
pe_off = struct.unpack('<I', hdr)[0]
|
|
img_sz = struct.unpack('<I', rd(h, 0x00400000 + pe_off + 4 + 20 + 56, 4))[0]
|
|
print(f"AC image: 0x00400000 .. 0x{0x00400000+img_sz:08x} ({img_sz/1024/1024:.1f} MB)")
|
|
|
|
# Read entire AC image
|
|
ac = rd(h, 0x00400000, img_sz)
|
|
if not ac: print("can't read AC image"); sys.exit(2)
|
|
|
|
# Find import directory: data dir entry 1
|
|
data_dir_off = pe_off + 4 + 20 + 96 + 8 # 8 = entry 1 * 8 bytes per entry
|
|
import_rva, import_size = struct.unpack_from('<II', ac, data_dir_off)
|
|
print(f"Import dir RVA=0x{import_rva:x} size={import_size}")
|
|
|
|
# Walk import descriptors looking for d3d9.dll
|
|
off = import_rva
|
|
n_desc = 0
|
|
while off + 20 <= len(ac):
|
|
olt, ts, fc, name_rva, fthunk = struct.unpack_from('<IIIII', ac, off)
|
|
if olt == 0 and name_rva == 0 and fthunk == 0:
|
|
print(f"end of import desc at #{n_desc} (off=0x{off:x})")
|
|
break
|
|
if name_rva == 0:
|
|
off += 20; n_desc += 1
|
|
continue
|
|
name = ac[name_rva:name_rva+32].split(b'\x00', 1)[0].decode(errors='replace')
|
|
print(f" desc[{n_desc}] dll={name!r} IAT-RVA=0x{fthunk:x}")
|
|
n_desc += 1
|
|
if 'd3d9' in name.lower():
|
|
print(f"\nFound import for {name}")
|
|
print(f" OriginalFirstThunk RVA=0x{olt:x}")
|
|
print(f" FirstThunk (IAT) RVA=0x{fthunk:x}")
|
|
# Walk OLT to find Direct3DCreate9 index
|
|
oft = olt or fthunk
|
|
idx = 0
|
|
while oft + 4 <= len(ac):
|
|
entry = struct.unpack_from('<I', ac, oft)[0]
|
|
if entry == 0: break
|
|
if entry & 0x80000000:
|
|
func_name = f"<ord {entry & 0xFFFF}>"
|
|
else:
|
|
# entry is an RVA to a Hint/Name struct: hint(2) + name(asciiz)
|
|
func_name = ac[entry+2:entry+64].split(b'\x00', 1)[0].decode(errors='replace')
|
|
# Read the IAT slot value (runtime address)
|
|
iat_slot_va = 0x00400000 + fthunk + idx*4
|
|
iat_val = struct.unpack('<I', rd(h, iat_slot_va, 4))[0]
|
|
print(f" [{idx:2d}] {func_name:30s} IAT@0x{iat_slot_va:08x} -> 0x{iat_val:08x}")
|
|
if func_name == 'Direct3DCreate9':
|
|
# Use this to find d3d9.dll's range
|
|
mbi = MBI()
|
|
if k.VirtualQueryEx(h, iat_val, ctypes.byref(mbi), ctypes.sizeof(mbi)):
|
|
ab = mbi.AllocationBase or 0
|
|
# Get image size from PE
|
|
hdr2 = rd(h, ab + 0x3C, 4)
|
|
pe2 = struct.unpack('<I', hdr2)[0]
|
|
sz2 = struct.unpack('<I', rd(h, ab + pe2 + 4 + 20 + 56, 4))[0]
|
|
print(f"\n*** d3d9.dll loaded at 0x{ab:08x}, size {sz2}")
|
|
oft += 4
|
|
idx += 1
|
|
break
|
|
off += 20
|
|
else:
|
|
print("walk fell off")
|
|
|
|
k.CloseHandle(h)
|