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
78
tools/find_parent_null_writes.py
Normal file
78
tools/find_parent_null_writes.py
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
"""find_parent_null_writes.py <exe_path>
|
||||
Scan the .text section for instructions that null the field at offset +0x40
|
||||
of any register-pointed structure (CPhysicsObj's parent field).
|
||||
Patterns:
|
||||
c7 4? 40 00 00 00 00 mov dword ptr [reg+0x40], 0
|
||||
89 4? 40 mov [reg+0x40], reg (then we'd need to check for xor'd reg)
|
||||
c7 8? 40 00 00 00 00 00 00 00 mov [reg+0x40], 0 with disp32
|
||||
Reports each match's VA + 16 bytes of surrounding context."""
|
||||
import struct, sys
|
||||
|
||||
def parse_pe(path):
|
||||
with open(path, 'rb') as f:
|
||||
data = f.read()
|
||||
pe_off = struct.unpack_from('<I', data, 0x3C)[0]
|
||||
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
|
||||
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')
|
||||
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, rsize, raddr))
|
||||
return data, image_base, sections
|
||||
|
||||
def main():
|
||||
path = sys.argv[1]
|
||||
data, base, sections = parse_pe(path)
|
||||
|
||||
text = None
|
||||
for name, vaddr, rsize, raddr in sections:
|
||||
if name == '.text':
|
||||
text = (base + vaddr, data[raddr:raddr + rsize])
|
||||
break
|
||||
if not text:
|
||||
print("no .text"); sys.exit(2)
|
||||
text_va, text_bytes = text
|
||||
print(f".text @ 0x{text_va:08x} size={len(text_bytes)}")
|
||||
|
||||
# Pattern: c7 4? 40 00 00 00 00 (7 bytes — mov [reg+0x40], imm32 == 0)
|
||||
# The ? is the ModRM byte's low nibble = register encoding.
|
||||
# Modrm 0x40-0x47 = [reg+disp8]; we want 0x40 (disp8 follows = 0x40)
|
||||
# Wait: opcode c7 /0 = mov mem, imm32. ModRM for [reg+disp8] is 01 xxx 100 = 0x44 if base+SIB,
|
||||
# or 01 000 reg for [reg+disp8] = 0x40..0x47.
|
||||
# Actually the agent's site had: c7 46 40 00 00 00 00
|
||||
# c7 = mov reg/mem32, imm32 (opcode /0 in modrm)
|
||||
# 46 = modrm: mod=01 (disp8), reg=/0 (000), rm=110 (esi) → [esi+disp8]
|
||||
# 40 = disp8 = 0x40
|
||||
# 00 00 00 00 = imm32 = 0
|
||||
# So we want: c7 4r 40 00 00 00 00 where r ∈ {0..7} but r=4 (esp) means SIB follows.
|
||||
# Valid r: 0,1,2,3,5,6,7 (skipping esp)
|
||||
matches = []
|
||||
for off in range(len(text_bytes) - 7):
|
||||
b0 = text_bytes[off]
|
||||
if b0 != 0xC7: continue
|
||||
b1 = text_bytes[off + 1]
|
||||
if (b1 & 0xF8) != 0x40: continue # mod=01, reg=000, rm in low 3
|
||||
if b1 == 0x44: continue # rm=esp needs SIB
|
||||
if text_bytes[off + 2] != 0x40: continue # disp8 must be 0x40
|
||||
if text_bytes[off + 3:off + 7] != b'\x00\x00\x00\x00': continue
|
||||
va = text_va + off
|
||||
ctx = text_bytes[max(0, off-8): off+12].hex(' ')
|
||||
regs = ['eax','ecx','edx','ebx','esp','ebp','esi','edi']
|
||||
rm = b1 & 7
|
||||
matches.append((va, regs[rm], ctx))
|
||||
|
||||
print(f"\nFound {len(matches)} occurrences of mov dword ptr [reg+0x40], 0:")
|
||||
print()
|
||||
for va, reg, ctx in matches:
|
||||
print(f" 0x{va:08x} [{reg}+0x40] = 0 context: ...{ctx}...")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue