leakhunt/README.md
acbot 92ec4e5ecf Clean source + repo down to deliverables only
- Remove disabled patches from DLL source (v18/v19/v20/v23/v24
  prototype paths and all v25-v38 d3d9 investigation tooling).
  Production DLL ships v3b/v5/v11/v14/v22 + crash handler only.
- Strip repo to 16 files: README, .gitignore, DLL source (8 files),
  build.bat, prebuilt dist/leakfix.dll, two installer tools.
- Rewrite README around per-patch pseudo-C showing each bug + fix.
- Update dist/leakfix.dll to the cleaned-source build (SHA 99ab51fe).
2026-05-26 20:50:57 +02:00

199 lines
6 KiB
Markdown

# leakfix.dll — Asheron's Call retail client patches
Five memory-leak and crash-stability patches for `acclient.exe`
v0.0.11.6096 (End of Retail, January 2017), delivered as a DLL that's
loaded via a static PE-import added to acclient.exe.
Unpatched control client dies in ~26 hours from palette state exhaustion.
With these patches applied, clients reach 5+ days of uptime.
## Install
```
python tools/install_leakfix.py "C:\Turbine\Asheron's Call"
```
The installer adds a `.limport` PE section to `acclient.exe` that
imports `leakfix.dll`. Idempotent. Verifies with:
```
python tools/install_leakfix.py "C:\Turbine\Asheron's Call" verify
```
`leakfix.dll` must already be in the same directory as `acclient.exe`.
## What gets patched
All five patches are applied 30 seconds after process start (deferred so
Decal / UtilityBelt finish their own hooking first).
### v3b — palette over-increment
The palette allocator increments a refcount twice when applying a
modification but only decrements once. Result: every modified palette
leaks one refcount, palettes never reach 0, the cache grows
unbounded. On heavy-loot clients this caused the dominant leak class
(446 MB / 56k instances at ~26 h).
```c
// Before:
void makeModifiedPalette(...) {
cache_entry->refcount += 1; // first increment — correct
...
cache_entry->refcount += 1; // <-- bug: second increment
...
}
// After: both `inc dword ptr [reg+0x24]` instructions NOP'd out.
// Refcount now matches release-side decrements; entries reach 0 and free.
```
Sites: `0x0053EFFE` (3 bytes → `90 90 90`), `0x0053F19C` (3 bytes → `90 90 90`).
### v5 — RenderSurface / RenderTexture PurgeResource override
`GraphicsResource::PurgeResource` is a virtual that returns 1 (the
"purged" indicator) without actually doing anything in the base class:
```c
// Before — slot 2 of RenderSurface and RenderTexture vtables both
// pointed at this no-op stub:
int GraphicsResource::PurgeResource() { return 1; }
```
The engine then deletes the entry from its purge list, marking the
resource "freed" while the D3D handle and shell still exist. Over
time these accumulate.
```c
// After — vtable slot redirected to our thunk:
int our_purge_thunk(this) {
this->Destroy(); // actually releases the D3D resource + shell state
return 1;
}
```
Vtable slot patches at `0x0079A684` (RenderSurface) and `0x0079C1A0`
(RenderTexture); thunks call the existing `RenderSurface::Destroy` at
`0x00444540` and `RenderTexture::Destroy` at `0x0044C4F0`.
### v11 — dangling-pointer crash guards
Two sites that dereference pointers that can already be freed. The
engine's existing code has the right "NULL check then skip" structure
nearby, but the wrong branch target / instruction order:
**Site 1 (`0x00587126`) — hash walk in `delete_contents`:**
```c
// Before — when slot has been freed, jumps to the wrong "done" label
// and re-dereferences the freed pointer:
if (slot == NULL) goto continue_walk; // intent
// actually compiled as: goto deref_slot; // bug
// After: JMP offset patched (EB 07 -> EB 42) so the NULL case skips
// the deref entirely and continues to the next hash bucket.
```
**Site 2 (`0x005E565D`) — `~GXTri3Mesh` vtable slot 0:**
```c
// Before — the destructor calls vtable[0] before nulling the field:
mov ecx, [eax] // load vtable
push eax
call [ecx + 8] // dispatch — AVs if eax was already freed
mov [esi+8], ebx // then clears the field
// After — reorder so the field is cleared first; the now-redundant
// virtual call is NOP'd:
mov [esi+8], ebx
nop nop nop nop nop nop
```
### v14 — `CEnvCell::Destroy` ClipPlaneList leak
When a cell unloads, the engine zeroes out the count of clip planes
but never frees the heap-allocated `ClipPlaneList` it points to:
```c
// Before — pseudo-C of the 18-byte buggy block at 0x0052E661:
ClipPlaneList** outer = this->cplanes; // [esi+0xDC]
if (outer) {
ClipPlaneList* inner = *outer;
if (inner) *outer = 0; // !! leaks `inner` and `outer`
}
```
```c
// After — JMP rel32 replaces the 18 bytes; thunk does the real free:
if (this->cplanes) {
if (*this->cplanes) {
(*this->cplanes)->~ClipPlaneList();
operator delete(*this->cplanes);
}
operator delete[](this->cplanes);
this->cplanes = nullptr;
}
```
### v22 — unpacker stale-pointer SEH guard
A small inline message-unpacker function dereferences a pointer that
the server can cause to be freed mid-execution. On 2026-05-21 a
server message simultaneously crashed 5 clients with the same AV.
```c
// The vulnerable function at 0x00526A50 (73 bytes):
int unpack(this, src, count) {
if (count < 0x10) return 0;
this->buf[0] = *src->ptr++;
this->buf[1] = *src->ptr++;
this->buf[2] = *src->ptr++;
this->buf[3] = *src->ptr++; // <-- AVs here when src->ptr is stale
return 1;
}
```
The fix copies the 73-byte body to executable memory, then patches
the original entry with a JMP into a wrapper that runs the copy
inside `__try / __except`:
```c
int unpacker_wrapper(this, src, count) {
__try {
return original_copy_in_heap(this, src, count);
} __except (EXCEPTION_EXECUTE_HANDLER) {
// engine's existing "count < 0x10" failure path
return 0;
}
}
```
## Build from source
Requires Visual Studio 2022 Build Tools (x86 toolchain).
```
dll\leakfix\build.bat
```
Output: `dll\leakfix\build\leakfix.dll` (~117 KB).
## Layout
```
README.md
dll/leakfix/
build.bat # one-line MSVC build
dist/leakfix.dll # prebuilt artifact (SHA 99ab51fe...)
src/
dllmain.cpp # DllMain + 30s deferred-patch thread
patches.cpp # apply_v3b/v5/v11/v14/v22
thunks.cpp # runtime thunks called by patched code
ac_addrs.h # EoR addresses
instr.cpp # crash handler (writes minidump on AV)
logging.cpp # log file writer
tools/
install_leakfix.py # patch acclient.exe to import leakfix.dll
check_acclient_imports.py # verify the import is present
```