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>
66 lines
2.3 KiB
C++
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;
|
|
}
|