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,72 @@
// thunks.cpp — runtime replacements called by AC into our DLL
#include "thunks.h"
#include "ac_addrs.h"
// ===== v5 — replacement PurgeResource for RenderSurface / RenderTexture =====
//
// Vtable slots use thiscall (ECX = this). MSVC __fastcall(arg1, arg2)
// receives arg1 in ECX and arg2 in EDX. EDX is scratch from the caller
// and isn't used, so we make it an unused parameter.
//
// Effect: instead of the no-op stub `mov al,1; ret`, we now actually
// call Destroy() on the resource (frees its D3D handle + heap state)
// then return 1 so PurgeOldResources marks it cleanly purged.
typedef void (__fastcall *destroy_fn_t)(void* self, void* edx_unused);
extern "C" int __fastcall purge_rendersurface_thunk(void* self, void* /*edx*/) {
((destroy_fn_t)ac::V5_RS_DESTROY_VA)(self, 0);
return 1;
}
extern "C" int __fastcall purge_rendertexture_thunk(void* self, void* /*edx*/) {
((destroy_fn_t)ac::V5_RT_DESTROY_VA)(self, 0);
return 1;
}
// ===== v14 — CEnvCell::Destroy ClipPlaneList cleanup =====
//
// EoR's CEnvCell::Destroy contains an 18-byte cleanup block at
// 0x0052E661 that only zeros cplane_num without freeing the underlying
// ClipPlaneList object. We replace those 18 bytes with a 5-byte
// JMP rel32 into the naked thunk below + 13 NOPs.
//
// Register context at entry (preserved from caller):
// esi = `this` (CEnvCell)
// ebx = 0 (cleared earlier in Destroy — relied on by the original
// buggy `mov [eax], ebx`)
// edi/ebp = live in surrounding loop
//
// On exit, we JMP to V14_RESUME_VA (the instruction immediately after
// the 18-byte block).
extern "C" __declspec(naked) void v14_clipplane_cleanup_thunk() {
__asm {
pushad ; preserve everything
mov edi, [esi + 0xDC] ; outer ClipPlaneList wrapper ptr
test edi, edi
jz done
mov ecx, [edi] ; inner ClipPlaneList ptr
test ecx, ecx
jz free_outer
// Free the inner ClipPlaneList properly
push ecx
mov eax, ac::V14_CLIPPLANELIST_DTOR
call eax ; ClipPlaneList::~ClipPlaneList (thiscall)
pop ecx
push ecx
mov eax, ac::V14_OPERATOR_DELETE
call eax ; operator delete(inner)
add esp, 4
free_outer:
push edi
mov eax, ac::V14_OPERATOR_DELETE_ARR
call eax ; operator delete[](outer)
add esp, 4
mov dword ptr [esi + 0xDC], 0 ; clear back-pointer
done:
popad
push ac::V14_RESUME_VA ; jmp to resume point
ret
}
}