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
103
dll/leakfix/stable/src.stable/dllmain.cpp
Normal file
103
dll/leakfix/stable/src.stable/dllmain.cpp
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
// 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue