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
55
tools/find_vtable_xrefs.py
Normal file
55
tools/find_vtable_xrefs.py
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
"""find_vtable_xrefs.py <exe_path> <vtable_va>
|
||||
Scan a PE for code/data references to a vtable VA.
|
||||
A constructor's vtable-store is the canonical 'who owns this vtable' xref.
|
||||
Pure stdlib — parses PE headers by hand."""
|
||||
import struct, sys
|
||||
|
||||
def parse_pe(path):
|
||||
with open(path, 'rb') as f:
|
||||
data = f.read()
|
||||
if data[:2] != b'MZ': raise ValueError("not PE")
|
||||
pe_off = struct.unpack_from('<I', data, 0x3C)[0]
|
||||
if data[pe_off:pe_off+4] != b'PE\0\0': raise ValueError("no PE sig")
|
||||
coff = pe_off + 4
|
||||
n_sections = struct.unpack_from('<H', data, coff + 2)[0]
|
||||
opt_size = struct.unpack_from('<H', data, coff + 16)[0]
|
||||
opt_off = coff + 20
|
||||
# PE32 optional header: ImageBase at +28
|
||||
image_base = struct.unpack_from('<I', data, opt_off + 28)[0]
|
||||
sec_off = opt_off + opt_size
|
||||
sections = []
|
||||
for i in range(n_sections):
|
||||
s = sec_off + i * 40
|
||||
name = data[s:s+8].rstrip(b'\x00').decode(errors='replace')
|
||||
vsize = struct.unpack_from('<I', data, s + 8)[0]
|
||||
vaddr = struct.unpack_from('<I', data, s + 12)[0]
|
||||
rsize = struct.unpack_from('<I', data, s + 16)[0]
|
||||
raddr = struct.unpack_from('<I', data, s + 20)[0]
|
||||
sections.append((name, vaddr, vsize, raddr, rsize))
|
||||
return data, image_base, sections
|
||||
|
||||
def main():
|
||||
path = sys.argv[1]
|
||||
vt = int(sys.argv[2], 0)
|
||||
needle = struct.pack('<I', vt)
|
||||
|
||||
data, base, sections = parse_pe(path)
|
||||
print(f"image base = 0x{base:08x}")
|
||||
|
||||
for name, vaddr, vsize, raddr, rsize in sections:
|
||||
sec_va = base + vaddr
|
||||
sec_data = data[raddr:raddr + rsize]
|
||||
n = 0
|
||||
for m_off in range(0, len(sec_data) - 3):
|
||||
if sec_data[m_off:m_off+4] == needle:
|
||||
va = sec_va + m_off
|
||||
lo = max(0, m_off - 8)
|
||||
ctx = sec_data[lo: m_off+12].hex(' ')
|
||||
print(f" [{name:8s}] xref @ 0x{va:08x}: ...{ctx}...")
|
||||
n += 1
|
||||
if n >= 80:
|
||||
print(f" (stopping after 80 in {name})")
|
||||
break
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue