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>
72 lines
2.7 KiB
C++
72 lines
2.7 KiB
C++
// 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
|
|
}
|
|
}
|