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>
103 lines
3.8 KiB
C++
103 lines
3.8 KiB
C++
// dllmain.cpp — leakfix.dll entry point
|
|
//
|
|
// iter-5 (2026-05-20): patch application is deferred to a worker
|
|
// thread that sleeps ~30 seconds before applying. This matches the
|
|
// timing of the Python runtime patcher (tools/fleet_monitor.sh),
|
|
// which lands its patches well after Decal init is complete. The
|
|
// PE-import-load → DllMain → immediate apply_all_patches sequence
|
|
// used in iter-1..iter-4 lost the race with Decal's own hook
|
|
// installation and crashed some accounts (Unkle Leo most reliably).
|
|
// See feedback_dll_load_order_conflict.md.
|
|
//
|
|
// The SEH crash handler is still installed immediately so any
|
|
// crashes during the 30s window (including ones caused by Decal)
|
|
// are captured.
|
|
#include <windows.h>
|
|
#include "logging.h"
|
|
#include "patches.h"
|
|
#include "instr.h"
|
|
|
|
namespace {
|
|
|
|
bool g_skip_patches = false;
|
|
|
|
DWORD WINAPI deferred_patch_thread(LPVOID) {
|
|
// Give Decal / UtilityBelt time to finish their own hook
|
|
// installation before we lay our patches on top. 30s matches
|
|
// the Python cascade's observed-good timing.
|
|
Sleep(30000);
|
|
leakfix::logf("deferred-patch thread: woke after 30s, applying patches");
|
|
if (g_skip_patches) {
|
|
leakfix::logf("LEAKFIX_NO_PATCHES=1 — skipping patch application (diagnostic mode)");
|
|
} else {
|
|
leakfix::apply_all_patches();
|
|
}
|
|
leakfix::instr_start_periodic_scan();
|
|
return 0;
|
|
}
|
|
|
|
void on_attach() {
|
|
char dll_path[MAX_PATH] = {0};
|
|
GetModuleFileNameA((HMODULE)GetModuleHandleA("leakfix.dll"), dll_path, MAX_PATH);
|
|
|
|
// Log next to the DLL itself
|
|
char log_path[MAX_PATH] = {0};
|
|
char* slash = nullptr;
|
|
for (char* p = dll_path; *p; ++p) if (*p == '\\' || *p == '/') slash = p;
|
|
if (slash) {
|
|
size_t prefix = (size_t)(slash - dll_path) + 1;
|
|
memcpy(log_path, dll_path, prefix);
|
|
strcpy(log_path + prefix, "leakfix.log");
|
|
} else {
|
|
strcpy(log_path, "leakfix.log");
|
|
}
|
|
leakfix::log_init(log_path);
|
|
leakfix::logf("attach: dll=%s (iter-5 deferred-patch)", dll_path);
|
|
|
|
// Kill switch — set LEAKFIX_NO_PATCHES=1 in env to skip patch
|
|
// application. Instrumentation still runs. Used to bisect crashes:
|
|
// if the no-patches variant survives, the patches are the trigger.
|
|
char no_patches[8] = {0};
|
|
GetEnvironmentVariableA("LEAKFIX_NO_PATCHES", no_patches, sizeof(no_patches));
|
|
g_skip_patches = (no_patches[0] == '1');
|
|
|
|
// Crash handler installed immediately so the 30s pre-patch window
|
|
// is still observable if Decal/UB crashes the process.
|
|
leakfix::instr_install_crash_handler();
|
|
|
|
HANDLE h = CreateThread(nullptr, 0, deferred_patch_thread, nullptr, 0, nullptr);
|
|
if (h) {
|
|
CloseHandle(h);
|
|
leakfix::logf("deferred-patch thread spawned");
|
|
} else {
|
|
// CreateThread failure is extraordinary — fall back to the
|
|
// old in-DllMain apply so we at least get patches eventually.
|
|
leakfix::logf("CreateThread failed (err=%lu) — falling back to in-DllMain apply",
|
|
GetLastError());
|
|
if (!g_skip_patches) leakfix::apply_all_patches();
|
|
leakfix::instr_start_periodic_scan();
|
|
}
|
|
}
|
|
} // anon
|
|
|
|
// Exported stub so PE-import-table patching of acclient.exe can name
|
|
// a real symbol for the OS loader to resolve. Doing nothing is fine —
|
|
// just being present in the DLL is what makes the import valid.
|
|
extern "C" __declspec(dllexport) int __cdecl leakfix_init() {
|
|
return 0;
|
|
}
|
|
|
|
extern "C" BOOL APIENTRY DllMain(HMODULE h, DWORD reason, LPVOID) {
|
|
switch (reason) {
|
|
case DLL_PROCESS_ATTACH:
|
|
DisableThreadLibraryCalls(h);
|
|
on_attach();
|
|
break;
|
|
case DLL_PROCESS_DETACH:
|
|
leakfix::instr_stop_periodic_scan();
|
|
leakfix::logf("detach");
|
|
leakfix::log_close();
|
|
break;
|
|
}
|
|
return TRUE;
|
|
}
|