leakhunt/dll/leakfix/injector/inject.cpp
acbot 57b5e43d0e 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>
2026-05-23 21:07:58 +02:00

66 lines
2.3 KiB
C++

// inject.cpp — load leakfix.dll into a running acclient.exe PID.
//
// Usage: inject.exe <pid> <abs_path_to_leakfix.dll>
//
// Mechanism: OpenProcess + VirtualAllocEx + WriteProcessMemory +
// CreateRemoteThread(LoadLibraryA). Standard Win32 DLL injection.
#include <windows.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
int main(int argc, char** argv) {
if (argc != 3) {
std::printf("usage: %s <pid> <dll_path>\n", argv[0]);
return 1;
}
DWORD pid = (DWORD)std::strtoul(argv[1], nullptr, 10);
const char* dll = argv[2];
HANDLE h = OpenProcess(
PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION |
PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ,
FALSE, pid);
if (!h) {
std::printf("OpenProcess(%lu) failed err=%lu\n", pid, GetLastError());
return 2;
}
size_t path_len = std::strlen(dll) + 1;
void* remote = VirtualAllocEx(h, nullptr, path_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!remote) {
std::printf("VirtualAllocEx failed err=%lu\n", GetLastError());
CloseHandle(h); return 3;
}
SIZE_T written = 0;
if (!WriteProcessMemory(h, remote, dll, path_len, &written)) {
std::printf("WriteProcessMemory failed err=%lu\n", GetLastError());
VirtualFreeEx(h, remote, 0, MEM_RELEASE); CloseHandle(h); return 4;
}
HMODULE k32 = GetModuleHandleA("kernel32.dll");
LPTHREAD_START_ROUTINE loadlib = (LPTHREAD_START_ROUTINE)GetProcAddress(k32, "LoadLibraryA");
if (!loadlib) {
std::printf("GetProcAddress(LoadLibraryA) failed err=%lu\n", GetLastError());
VirtualFreeEx(h, remote, 0, MEM_RELEASE); CloseHandle(h); return 5;
}
DWORD tid = 0;
HANDLE thr = CreateRemoteThread(h, nullptr, 0, loadlib, remote, 0, &tid);
if (!thr) {
std::printf("CreateRemoteThread failed err=%lu\n", GetLastError());
VirtualFreeEx(h, remote, 0, MEM_RELEASE); CloseHandle(h); return 6;
}
std::printf("injected; remote tid=%lu, waiting for LoadLibraryA to return...\n", tid);
WaitForSingleObject(thr, 30000);
DWORD exit_code = 0;
GetExitCodeThread(thr, &exit_code);
std::printf("LoadLibraryA returned 0x%08lx (non-zero = HMODULE)\n", exit_code);
CloseHandle(thr);
VirtualFreeEx(h, remote, 0, MEM_RELEASE);
CloseHandle(h);
return exit_code ? 0 : 7;
}