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
195
dll/leakfix/stable/src.iter3/patches.cpp
Normal file
195
dll/leakfix/stable/src.iter3/patches.cpp
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
// patches.cpp — apply v3b, v5, v11, v12, v14 inline to our own process
|
||||
#include "patches.h"
|
||||
#include "logging.h"
|
||||
#include "thunks.h"
|
||||
#include "ac_addrs.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
|
||||
using namespace leakfix;
|
||||
|
||||
namespace {
|
||||
|
||||
// Copy `data` to absolute address `addr`, flipping page protection.
|
||||
bool write_memory(uintptr_t addr, const void* data, size_t len) {
|
||||
DWORD old = 0;
|
||||
if (!VirtualProtect((void*)addr, len, PAGE_EXECUTE_READWRITE, &old)) {
|
||||
logf(" VirtualProtect(0x%08x, %u) failed err=%lu", addr, (unsigned)len, GetLastError());
|
||||
return false;
|
||||
}
|
||||
std::memcpy((void*)addr, data, len);
|
||||
DWORD restored = 0;
|
||||
VirtualProtect((void*)addr, len, old, &restored);
|
||||
FlushInstructionCache(GetCurrentProcess(), (void*)addr, len);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool bytes_equal(uintptr_t addr, const void* expected, size_t len) {
|
||||
return std::memcmp((void*)addr, expected, len) == 0;
|
||||
}
|
||||
|
||||
void hexdump_short(uintptr_t addr, size_t n, char* out, size_t out_sz) {
|
||||
const uint8_t* p = (const uint8_t*)addr;
|
||||
size_t used = 0;
|
||||
for (size_t i = 0; i < n && used + 3 < out_sz; ++i) {
|
||||
used += (size_t)std::snprintf(out + used, out_sz - used, "%02x", p[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Write a 5-byte JMP rel32 at `at` targeting `target`. Pad remaining bytes
|
||||
// up to `total_replace` with 0x90 NOPs.
|
||||
bool write_jmp_rel32(uintptr_t at, uintptr_t target, size_t total_replace) {
|
||||
uint8_t buf[64];
|
||||
if (total_replace > sizeof(buf)) return false;
|
||||
int32_t rel = (int32_t)(target - (at + 5));
|
||||
buf[0] = 0xE9;
|
||||
std::memcpy(buf + 1, &rel, 4);
|
||||
std::memset(buf + 5, 0x90, total_replace - 5);
|
||||
return write_memory(at, buf, total_replace);
|
||||
}
|
||||
|
||||
} // anon namespace
|
||||
|
||||
|
||||
namespace leakfix {
|
||||
|
||||
// ===== v3b =====
|
||||
bool apply_v3b() {
|
||||
const uint8_t nops[3] = { 0x90, 0x90, 0x90 };
|
||||
const uint8_t orig1[3] = { 0xff, 0x40, 0x24 }; // inc dword [eax+0x24]
|
||||
const uint8_t orig2[3] = { 0xff, 0x46, 0x24 }; // inc dword [esi+0x24]
|
||||
|
||||
if (bytes_equal(ac::V3B_SITE_1, nops, 3) && bytes_equal(ac::V3B_SITE_2, nops, 3)) {
|
||||
logf("v3b: already applied");
|
||||
return true;
|
||||
}
|
||||
if (!bytes_equal(ac::V3B_SITE_1, orig1, 3)) {
|
||||
char h[16]; hexdump_short(ac::V3B_SITE_1, 3, h, sizeof(h));
|
||||
logf("v3b: site1 unexpected bytes %s — refusing", h);
|
||||
return false;
|
||||
}
|
||||
if (!bytes_equal(ac::V3B_SITE_2, orig2, 3)) {
|
||||
char h[16]; hexdump_short(ac::V3B_SITE_2, 3, h, sizeof(h));
|
||||
logf("v3b: site2 unexpected bytes %s — refusing", h);
|
||||
return false;
|
||||
}
|
||||
write_memory(ac::V3B_SITE_1, nops, 3);
|
||||
write_memory(ac::V3B_SITE_2, nops, 3);
|
||||
logf("v3b: applied (NOPs at 0x%08x + 0x%08x)", ac::V3B_SITE_1, ac::V3B_SITE_2);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===== v5 =====
|
||||
bool apply_v5() {
|
||||
uintptr_t rs_cur = *(uintptr_t*)ac::V5_RS_VTABLE_SLOT_2;
|
||||
uintptr_t rt_cur = *(uintptr_t*)ac::V5_RT_VTABLE_SLOT_2;
|
||||
uintptr_t rs_new = (uintptr_t)&purge_rendersurface_thunk;
|
||||
uintptr_t rt_new = (uintptr_t)&purge_rendertexture_thunk;
|
||||
|
||||
bool rs_done = (rs_cur != ac::V5_NOOP_STUB_VA);
|
||||
bool rt_done = (rt_cur != ac::V5_NOOP_STUB_VA);
|
||||
|
||||
if (!rs_done) {
|
||||
if (rs_cur != ac::V5_NOOP_STUB_VA) {
|
||||
logf("v5: RS slot already redirected (0x%08x); not overwriting", rs_cur);
|
||||
} else {
|
||||
write_memory(ac::V5_RS_VTABLE_SLOT_2, &rs_new, 4);
|
||||
logf("v5: RS vtable slot -> 0x%08x", rs_new);
|
||||
}
|
||||
} else {
|
||||
logf("v5: RS slot already non-default (0x%08x) — skipping", rs_cur);
|
||||
}
|
||||
|
||||
if (!rt_done) {
|
||||
write_memory(ac::V5_RT_VTABLE_SLOT_2, &rt_new, 4);
|
||||
logf("v5: RT vtable slot -> 0x%08x", rt_new);
|
||||
} else {
|
||||
logf("v5: RT slot already non-default (0x%08x) — skipping", rt_cur);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===== v11 =====
|
||||
bool apply_v11() {
|
||||
// Site 1: 2-byte rewrite of a JMP target
|
||||
const uint8_t s1_orig[2] = { 0xEB, 0x07 };
|
||||
const uint8_t s1_patched[2] = { 0xEB, 0x42 };
|
||||
// Site 2: 9-byte rewrite for ~GXTri3Mesh NULL-check
|
||||
const uint8_t s2_orig[9] = { 0x8B, 0x08, 0x50, 0xFF, 0x51, 0x08, 0x89, 0x5E, 0x08 };
|
||||
const uint8_t s2_patched[9] = { 0x89, 0x5E, 0x08, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 };
|
||||
|
||||
if (bytes_equal(ac::V11_SITE_1_VA, s1_patched, 2)) {
|
||||
logf("v11: site1 already patched");
|
||||
} else if (bytes_equal(ac::V11_SITE_1_VA, s1_orig, 2)) {
|
||||
write_memory(ac::V11_SITE_1_VA, s1_patched, 2);
|
||||
logf("v11: site1 patched");
|
||||
} else {
|
||||
char h[8]; hexdump_short(ac::V11_SITE_1_VA, 2, h, sizeof(h));
|
||||
logf("v11: site1 unexpected %s — skipping", h);
|
||||
}
|
||||
|
||||
if (bytes_equal(ac::V11_SITE_2_VA, s2_patched, 9)) {
|
||||
logf("v11: site2 already patched");
|
||||
} else if (bytes_equal(ac::V11_SITE_2_VA, s2_orig, 9)) {
|
||||
write_memory(ac::V11_SITE_2_VA, s2_patched, 9);
|
||||
logf("v11: site2 patched");
|
||||
} else {
|
||||
char h[24]; hexdump_short(ac::V11_SITE_2_VA, 9, h, sizeof(h));
|
||||
logf("v11: site2 unexpected %s — skipping", h);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===== v12 RETIRED =====
|
||||
// v12 was designed against post-Decal in-memory bytes that don't match
|
||||
// the on-disk binary. When the leakfix.dll loads at PE-import time (before
|
||||
// Decal init), it sees the truly-original bytes and v12 would refuse.
|
||||
// When the Python patcher ran later against a running PID, it saw
|
||||
// Decal-modified bytes that happened to match its expected pattern and
|
||||
// applied a duplicate range check — adding no protection beyond what
|
||||
// Decal already provides. Neither variant prevented the Shadow/Frank
|
||||
// stale-heap-pointer crashes. v12 removed.
|
||||
|
||||
// ===== v14 =====
|
||||
bool apply_v14() {
|
||||
static const uint8_t ORIG[18] = {
|
||||
0x8B, 0x86, 0xDC, 0x00, 0x00, 0x00, // mov eax, [esi+0xDC]
|
||||
0x3B, 0xC3, // cmp eax, ebx
|
||||
0x74, 0x08, // jz +8
|
||||
0x8B, 0x00, // mov eax, [eax]
|
||||
0x3B, 0xC3, // cmp eax, ebx
|
||||
0x74, 0x02, // jz +2
|
||||
0x89, 0x18, // mov [eax], ebx <- the broken "fix"
|
||||
};
|
||||
|
||||
// If already patched, the first byte is 0xE9 (our JMP).
|
||||
uint8_t cur = *(uint8_t*)ac::V14_PATCH_SITE_VA;
|
||||
if (cur == 0xE9) {
|
||||
logf("v14: already applied");
|
||||
return true;
|
||||
}
|
||||
if (!bytes_equal(ac::V14_PATCH_SITE_VA, ORIG, 18)) {
|
||||
char h[40]; hexdump_short(ac::V14_PATCH_SITE_VA, 18, h, sizeof(h));
|
||||
logf("v14: site unexpected bytes %s — refusing", h);
|
||||
return false;
|
||||
}
|
||||
uintptr_t thunk_va = (uintptr_t)&v14_clipplane_cleanup_thunk;
|
||||
if (!write_jmp_rel32(ac::V14_PATCH_SITE_VA, thunk_va, 18)) return false;
|
||||
logf("v14: applied (JMP rel32 -> 0x%08x, thunk in leakfix.dll)", thunk_va);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool apply_all_patches() {
|
||||
bool ok = true;
|
||||
ok &= apply_v3b();
|
||||
ok &= apply_v11();
|
||||
ok &= apply_v5();
|
||||
ok &= apply_v14();
|
||||
logf("all-patches result: %s", ok ? "OK" : "PARTIAL");
|
||||
return ok;
|
||||
}
|
||||
|
||||
} // namespace leakfix
|
||||
Loading…
Add table
Add a link
Reference in a new issue