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
568
dll/leakfix/stable/src.stable/instr.cpp
Normal file
568
dll/leakfix/stable/src.stable/instr.cpp
Normal file
|
|
@ -0,0 +1,568 @@
|
|||
// instr.cpp — crash dump + periodic instance-count scanner
|
||||
#include "instr.h"
|
||||
#include "logging.h"
|
||||
#include "ac_addrs.h"
|
||||
|
||||
#include <windows.h>
|
||||
#include <dbghelp.h>
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
#pragma comment(lib, "dbghelp.lib")
|
||||
|
||||
namespace {
|
||||
|
||||
// ===== Crash-dump =====
|
||||
|
||||
LPTOP_LEVEL_EXCEPTION_FILTER g_prev_filter = nullptr;
|
||||
|
||||
// Returns directory of our DLL, ending with backslash.
|
||||
void get_dll_dir(char* out, size_t out_sz) {
|
||||
HMODULE h = nullptr;
|
||||
GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
||||
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
(LPCSTR)&get_dll_dir, &h);
|
||||
char path[MAX_PATH] = {0};
|
||||
GetModuleFileNameA(h, path, MAX_PATH);
|
||||
size_t n = strlen(path);
|
||||
while (n > 0 && path[n-1] != '\\' && path[n-1] != '/') --n;
|
||||
if (n >= out_sz) n = out_sz - 1;
|
||||
memcpy(out, path, n);
|
||||
out[n] = '\0';
|
||||
}
|
||||
|
||||
LONG WINAPI top_level_handler(EXCEPTION_POINTERS* ep) {
|
||||
char dir[MAX_PATH]; get_dll_dir(dir, sizeof(dir));
|
||||
SYSTEMTIME st; GetLocalTime(&st);
|
||||
char path[MAX_PATH];
|
||||
std::snprintf(path, sizeof(path),
|
||||
"%sleakfix_crash_%lu_%04d%02d%02d_%02d%02d%02d.dmp",
|
||||
dir, GetCurrentProcessId(),
|
||||
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
|
||||
|
||||
leakfix::logf("UNHANDLED EXCEPTION code=0x%08lx addr=0x%p — writing %s",
|
||||
ep->ExceptionRecord->ExceptionCode,
|
||||
ep->ExceptionRecord->ExceptionAddress, path);
|
||||
|
||||
HANDLE hf = CreateFileA(path, GENERIC_WRITE, 0, nullptr,
|
||||
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (hf != INVALID_HANDLE_VALUE) {
|
||||
MINIDUMP_EXCEPTION_INFORMATION mei{};
|
||||
mei.ThreadId = GetCurrentThreadId();
|
||||
mei.ExceptionPointers = ep;
|
||||
mei.ClientPointers = FALSE;
|
||||
// MiniDumpNormal does NOT walk the heap — safer when crash is
|
||||
// heap-corruption. Includes thread stacks + module info which
|
||||
// is enough to identify the fault site. Previous attempts with
|
||||
// MiniDumpWithDataSegs produced 0-byte files because the
|
||||
// corrupted heap broke MiniDumpWriteDump mid-walk.
|
||||
BOOL ok = MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(),
|
||||
hf, MiniDumpNormal, &mei, nullptr, nullptr);
|
||||
DWORD dump_err = ok ? 0 : GetLastError();
|
||||
CloseHandle(hf);
|
||||
leakfix::logf("MiniDumpWriteDump: %s err=%lu", ok ? "ok" : "failed", dump_err);
|
||||
// If even MiniDumpNormal fails, write a tiny text file with the
|
||||
// bare minimum info — exception code, address, register state.
|
||||
if (!ok) {
|
||||
char fallback_path[MAX_PATH];
|
||||
std::snprintf(fallback_path, sizeof(fallback_path), "%s.txt", path);
|
||||
HANDLE tf = CreateFileA(fallback_path, GENERIC_WRITE, 0, nullptr,
|
||||
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (tf != INVALID_HANDLE_VALUE) {
|
||||
CONTEXT* ctx = ep->ContextRecord;
|
||||
char body[512];
|
||||
int n = std::snprintf(body, sizeof(body),
|
||||
"exception_code=0x%08lx\n"
|
||||
"exception_address=0x%p\n"
|
||||
"eax=0x%08lx ebx=0x%08lx ecx=0x%08lx edx=0x%08lx\n"
|
||||
"esi=0x%08lx edi=0x%08lx ebp=0x%08lx esp=0x%08lx\n"
|
||||
"eip=0x%08lx eflags=0x%08lx\n",
|
||||
ep->ExceptionRecord->ExceptionCode,
|
||||
ep->ExceptionRecord->ExceptionAddress,
|
||||
ctx->Eax, ctx->Ebx, ctx->Ecx, ctx->Edx,
|
||||
ctx->Esi, ctx->Edi, ctx->Ebp, ctx->Esp,
|
||||
ctx->Eip, ctx->EFlags);
|
||||
DWORD written = 0;
|
||||
WriteFile(tf, body, n, &written, nullptr);
|
||||
CloseHandle(tf);
|
||||
leakfix::logf("fallback text crash info -> %s", fallback_path);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
leakfix::logf("CreateFile failed err=%lu", GetLastError());
|
||||
}
|
||||
|
||||
// Chain to previous filter (lets Windows do its thing too).
|
||||
if (g_prev_filter) return g_prev_filter(ep);
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
// ===== Periodic scan =====
|
||||
|
||||
struct vt_entry { const char* name; uintptr_t vt; };
|
||||
|
||||
// Mirrors what tools/snapshot_compare.py tracks.
|
||||
const vt_entry VTABLES[] = {
|
||||
{"uiitem", 0x007C0498},
|
||||
{"palette", 0x007CAA08},
|
||||
{"cphysicsobj", 0x007C78EC},
|
||||
{"renderSurf", 0x0079A67C},
|
||||
{"renderSurfD3D", 0x00801A94},
|
||||
{"renderTexD3D", 0x00801A18},
|
||||
{"csurface", 0x007CA4DC},
|
||||
{"imgtex", 0x007CAB04},
|
||||
{"cgfxobj", 0x007CA418},
|
||||
{"d3dxmesh", 0x007ED3B0},
|
||||
{"position", 0x00797910},
|
||||
};
|
||||
constexpr size_t VT_COUNT = sizeof(VTABLES) / sizeof(VTABLES[0]);
|
||||
|
||||
HANDLE g_scan_thread = nullptr;
|
||||
HANDLE g_stop_event = nullptr;
|
||||
int g_prev_counts[VT_COUNT] = {0};
|
||||
bool g_have_prev = false;
|
||||
DWORD g_scan_count = 0;
|
||||
|
||||
// CPhysicsObj field offsets — VERIFIED against acclient.h + live sample
|
||||
// dumps from iter 2. LongHashData base = 12 bytes (vtable +
|
||||
// hash_next + id), then CPhysicsObj-specific fields follow.
|
||||
constexpr int CPHYS_VT_OFF = 0x00; // CPhysicsObj vt = 0x007C78EC
|
||||
constexpr int CPHYS_HASH_NEXT_OFF = 0x04; // LongHashData chain
|
||||
constexpr int CPHYS_ID_OFF = 0x08; // hash key
|
||||
constexpr int CPHYS_PARTARRAY_OFF = 0x10; // CPartArray*
|
||||
constexpr int CPHYS_PARENT_OFF = 0x40; // CPhysicsObj* parent
|
||||
constexpr int CPHYS_CHILDREN_OFF = 0x44; // CHILDLIST*
|
||||
constexpr int CPHYS_POSITION_OFF = 0x48; // Position (72 B)
|
||||
constexpr int CPHYS_CELL_OFF = 0x90; // CObjCell*
|
||||
constexpr int CPHYS_STATE_OFF = 0xA8; // unsigned int state
|
||||
constexpr int CPHYS_TRANSTATE_OFF = 0xAC; // unsigned int transient_state
|
||||
constexpr int CPHYS_MOVMGR_OFF = 0xC4; // MovementManager*
|
||||
constexpr int CPHYS_POSMGR_OFF = 0xC8; // PositionManager*
|
||||
constexpr int CPHYS_UPDATETIME_OFF= 0xD4; // long double (8 B at runtime)
|
||||
constexpr int CPHYS_HOOKS_OFF = 0x100; // PhysicsObjHook*
|
||||
constexpr int CPHYS_WEENIEOBJ_OFF = 0x12C; // CWeenieObject*
|
||||
|
||||
// Iter-4 destroy target
|
||||
constexpr uintptr_t CPHYSICSOBJ_DESTROY_VA = 0x005145D0;
|
||||
|
||||
// Engine global time pointer (TODO: verify offset/symbol) — used for
|
||||
// stale-update_time check. For now we just compare against a wall-time
|
||||
// estimate.
|
||||
|
||||
constexpr int CPHYS_SAMPLE_BYTES = 384; // dump first 384 B per sample (incl weenie_obj at +0x12C)
|
||||
constexpr int CPHYS_SAMPLE_COUNT = 3; // samples per scan
|
||||
|
||||
// Find up to `want` instances of vt 0x007C78EC; return how many recorded.
|
||||
int find_physobj_samples(uintptr_t* out_addrs, int want) {
|
||||
int found = 0;
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
uintptr_t addr = 0;
|
||||
while (found < want && VirtualQuery((LPCVOID)addr, &mbi, sizeof(mbi))) {
|
||||
bool committed = mbi.State == MEM_COMMIT;
|
||||
bool priv = mbi.Type == MEM_PRIVATE;
|
||||
DWORD prot = mbi.Protect & 0xFF;
|
||||
bool readable = (prot == PAGE_READWRITE || prot == PAGE_EXECUTE_READWRITE);
|
||||
if (committed && priv && readable) {
|
||||
__try {
|
||||
const uint32_t* p = (const uint32_t*)mbi.BaseAddress;
|
||||
size_t n = mbi.RegionSize / 4;
|
||||
for (size_t i = 0; i < n && found < want; ++i) {
|
||||
if (p[i] == 0x007C78EC) {
|
||||
out_addrs[found++] = (uintptr_t)(p + i);
|
||||
}
|
||||
}
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) {}
|
||||
}
|
||||
uintptr_t next = (uintptr_t)mbi.BaseAddress + mbi.RegionSize;
|
||||
if (next <= addr) break;
|
||||
addr = next;
|
||||
if (addr >= 0x80000000) break;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
void dump_physobj(uintptr_t obj_addr) {
|
||||
uint32_t buf[CPHYS_SAMPLE_BYTES / 4];
|
||||
__try {
|
||||
memcpy(buf, (const void*)obj_addr, CPHYS_SAMPLE_BYTES);
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
||||
leakfix::logf("sample physobj @ 0x%08x: read failed", obj_addr);
|
||||
return;
|
||||
}
|
||||
// Log as 4 DWORDs per line. Annotate each DWORD with a hint:
|
||||
// '0' if null, 'V' if it equals the CPhysicsObj vt (suggests parent),
|
||||
// 'C' if it equals a known CObjCell-family vt (suggests cell),
|
||||
// 'i' if in image range (.text/.rdata),
|
||||
// 'h' if in typical heap range.
|
||||
leakfix::logf("sample physobj @ 0x%08x:", obj_addr);
|
||||
for (int row = 0; row < CPHYS_SAMPLE_BYTES / 16; ++row) {
|
||||
char line[256];
|
||||
int n = std::snprintf(line, sizeof(line), " +0x%02x: ", row * 16);
|
||||
for (int col = 0; col < 4; ++col) {
|
||||
uint32_t v = buf[row * 4 + col];
|
||||
char tag = ' ';
|
||||
if (v == 0) tag = '0';
|
||||
else if (v == 0x007C78EC) tag = 'V'; // CPhysicsObj vt
|
||||
else if (v == 0x007ED3B0 || v == 0x007CA4F0) tag = 'C'; // CObjCell-family vt
|
||||
else if (v >= 0x00400000 && v < 0x00800000) tag = 'i'; // image range
|
||||
else if (v >= 0x01000000 && v < 0x40000000) tag = 'h'; // heap
|
||||
n += std::snprintf(line + n, sizeof(line) - n, "%08x%c ", v, tag);
|
||||
}
|
||||
leakfix::logf("%s", line);
|
||||
}
|
||||
}
|
||||
|
||||
// ITER 3 — for each CPhysicsObj instance, evaluate "safe to destroy"
|
||||
// predicates. Read-only — no mutation.
|
||||
//
|
||||
// Predicates evaluated (all on the SAME instance):
|
||||
// P_no_parent = (parent == NULL)
|
||||
// P_no_cell = (cell == NULL)
|
||||
// P_orphan_hash = (hash_next == NULL) [not linked into any hash chain]
|
||||
// P_no_part_array = (part_array == NULL)
|
||||
//
|
||||
// Logged combinations:
|
||||
// n_total = all CPhysicsObj found
|
||||
// n_no_parent = count where parent==NULL
|
||||
// n_no_cell = count where cell==NULL
|
||||
// n_orphan_hash = count where hash_next==NULL
|
||||
// n_both = count where parent==NULL AND cell==NULL (STRICT)
|
||||
// n_triple = count where parent==NULL AND cell==NULL AND hash_next==NULL
|
||||
//
|
||||
// The "triple" set is the candidate set we'd target for sweep — physobjs
|
||||
// that are not in any hash chain, not in any cell, and have no parent.
|
||||
// They are by definition unreachable from the engine's active state.
|
||||
void evaluate_predicates_and_dump_candidates() {
|
||||
int n_total = 0;
|
||||
int n_no_parent = 0;
|
||||
int n_no_cell = 0;
|
||||
int n_orphan_hash = 0;
|
||||
int n_both = 0;
|
||||
int n_triple = 0;
|
||||
|
||||
constexpr int CANDIDATE_DUMP_MAX = 3;
|
||||
uintptr_t candidates[CANDIDATE_DUMP_MAX] = {0};
|
||||
int n_candidates_recorded = 0;
|
||||
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
uintptr_t addr = 0;
|
||||
while (VirtualQuery((LPCVOID)addr, &mbi, sizeof(mbi))) {
|
||||
bool committed = mbi.State == MEM_COMMIT;
|
||||
bool priv = mbi.Type == MEM_PRIVATE;
|
||||
DWORD prot = mbi.Protect & 0xFF;
|
||||
bool readable = (prot == PAGE_READWRITE || prot == PAGE_EXECUTE_READWRITE);
|
||||
if (committed && priv && readable) {
|
||||
__try {
|
||||
const uint32_t* p = (const uint32_t*)mbi.BaseAddress;
|
||||
size_t n = mbi.RegionSize / 4;
|
||||
// Walk DWORDs; need at least 0xA8/4 == 42 DWORDs of headroom
|
||||
// for the deepest field we read.
|
||||
size_t need = (CPHYS_STATE_OFF / 4) + 1;
|
||||
if (n < need) {
|
||||
addr = (uintptr_t)mbi.BaseAddress + mbi.RegionSize;
|
||||
if (addr >= 0x80000000) break;
|
||||
continue;
|
||||
}
|
||||
for (size_t i = 0; i + need < n; ++i) {
|
||||
if (p[i] != 0x007C78EC) continue; // not CPhysicsObj
|
||||
++n_total;
|
||||
const uint32_t hash_next = p[i + (CPHYS_HASH_NEXT_OFF / 4)];
|
||||
const uint32_t parent = p[i + (CPHYS_PARENT_OFF / 4)];
|
||||
const uint32_t cell = p[i + (CPHYS_CELL_OFF / 4)];
|
||||
|
||||
const bool no_parent = (parent == 0);
|
||||
const bool no_cell = (cell == 0);
|
||||
const bool orphan_hash = (hash_next == 0);
|
||||
|
||||
if (no_parent) ++n_no_parent;
|
||||
if (no_cell) ++n_no_cell;
|
||||
if (orphan_hash) ++n_orphan_hash;
|
||||
if (no_parent && no_cell) ++n_both;
|
||||
if (no_parent && no_cell && orphan_hash) {
|
||||
++n_triple;
|
||||
if (n_candidates_recorded < CANDIDATE_DUMP_MAX) {
|
||||
candidates[n_candidates_recorded++] = (uintptr_t)(p + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) {}
|
||||
}
|
||||
uintptr_t next = (uintptr_t)mbi.BaseAddress + mbi.RegionSize;
|
||||
if (next <= addr) break;
|
||||
addr = next;
|
||||
if (addr >= 0x80000000) break;
|
||||
}
|
||||
|
||||
leakfix::logf("predicates: total=%d no_parent=%d no_cell=%d orphan_hash=%d "
|
||||
"both=%d triple=%d (candidates for sweep)",
|
||||
n_total, n_no_parent, n_no_cell, n_orphan_hash,
|
||||
n_both, n_triple);
|
||||
|
||||
// Dump first few strict candidates so we can sanity-check they look
|
||||
// like genuinely abandoned objects (no weenie, no part_array, etc.).
|
||||
for (int i = 0; i < n_candidates_recorded; ++i) {
|
||||
leakfix::logf("--- strict candidate %d ---", i);
|
||||
dump_physobj(candidates[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// ITER 4 — STRICT PREDICATES + DESTRUCTION SWEEP
|
||||
// =====================================================================
|
||||
//
|
||||
// Strict-candidate definition (all must hold):
|
||||
// - parent == NULL (+0x40)
|
||||
// - cell == NULL (+0x90)
|
||||
// - hash_next == NULL (+0x04)
|
||||
// - movement_manager == NULL (+0xC4)
|
||||
// - weenie_obj == NULL (+0x12C)
|
||||
// - state has no high bits set (high 16 bits == 0)
|
||||
// - transient_state == 0 (+0xAC)
|
||||
//
|
||||
// 4a: log candidates, no mutation
|
||||
// 4b: destroy 1 candidate per scan via CPhysicsObj::Destroy then
|
||||
// operator delete
|
||||
|
||||
typedef void (__fastcall *physobj_destroy_fn)(void* self, void* edx);
|
||||
|
||||
bool is_strict_abandoned(const uint32_t* p) {
|
||||
if (p[CPHYS_PARENT_OFF / 4] != 0) return false;
|
||||
if (p[CPHYS_CELL_OFF / 4] != 0) return false;
|
||||
if (p[CPHYS_HASH_NEXT_OFF / 4] != 0) return false;
|
||||
if (p[CPHYS_MOVMGR_OFF / 4] != 0) return false;
|
||||
if (p[CPHYS_WEENIEOBJ_OFF / 4] != 0) return false;
|
||||
const uint32_t state = p[CPHYS_STATE_OFF / 4];
|
||||
if ((state & 0xFFFF0000) != 0) return false;
|
||||
if (p[CPHYS_TRANSTATE_OFF / 4] != 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
DWORD g_total_destroyed = 0;
|
||||
bool g_sweep_armed = false; // becomes true after first 2 scans without
|
||||
// crashes — gives time to observe candidates
|
||||
// first, then mutate on subsequent scans
|
||||
|
||||
void evaluate_strict_and_optionally_destroy() {
|
||||
int n_total = 0;
|
||||
int n_strict = 0;
|
||||
uintptr_t first_candidate = 0;
|
||||
uintptr_t first_dump_candidate = 0;
|
||||
uintptr_t second_dump_candidate = 0;
|
||||
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
uintptr_t addr = 0;
|
||||
while (VirtualQuery((LPCVOID)addr, &mbi, sizeof(mbi))) {
|
||||
bool committed = mbi.State == MEM_COMMIT;
|
||||
bool priv = mbi.Type == MEM_PRIVATE;
|
||||
DWORD prot = mbi.Protect & 0xFF;
|
||||
bool readable = (prot == PAGE_READWRITE || prot == PAGE_EXECUTE_READWRITE);
|
||||
if (committed && priv && readable) {
|
||||
__try {
|
||||
const uint32_t* p = (const uint32_t*)mbi.BaseAddress;
|
||||
size_t n = mbi.RegionSize / 4;
|
||||
// Need to read up to +0x12C / 4 + 1 = 76 DWORDs
|
||||
size_t need = (CPHYS_WEENIEOBJ_OFF / 4) + 1;
|
||||
if (n < need) {
|
||||
addr = (uintptr_t)mbi.BaseAddress + mbi.RegionSize;
|
||||
if (addr >= 0x80000000) break;
|
||||
continue;
|
||||
}
|
||||
for (size_t i = 0; i + need < n; ++i) {
|
||||
if (p[i] != 0x007C78EC) continue;
|
||||
++n_total;
|
||||
if (!is_strict_abandoned(p + i)) continue;
|
||||
++n_strict;
|
||||
if (!first_candidate) first_candidate = (uintptr_t)(p + i);
|
||||
if (!first_dump_candidate) first_dump_candidate = (uintptr_t)(p + i);
|
||||
else if (!second_dump_candidate) second_dump_candidate = (uintptr_t)(p + i);
|
||||
}
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) {}
|
||||
}
|
||||
uintptr_t next = (uintptr_t)mbi.BaseAddress + mbi.RegionSize;
|
||||
if (next <= addr) break;
|
||||
addr = next;
|
||||
if (addr >= 0x80000000) break;
|
||||
}
|
||||
|
||||
leakfix::logf("strict-predicates: total=%d strict_candidates=%d destroyed_so_far=%lu armed=%d",
|
||||
n_total, n_strict, g_total_destroyed, g_sweep_armed ? 1 : 0);
|
||||
|
||||
// Always dump 2 candidates for forensic comparison
|
||||
if (first_dump_candidate) {
|
||||
leakfix::logf("--- strict-4 candidate #1 (would-destroy first) ---");
|
||||
dump_physobj(first_dump_candidate);
|
||||
}
|
||||
if (second_dump_candidate) {
|
||||
leakfix::logf("--- strict-4 candidate #2 ---");
|
||||
dump_physobj(second_dump_candidate);
|
||||
}
|
||||
|
||||
// Iter 4b — actual destruction. Only after sweep is armed AND a
|
||||
// candidate is available. Throttled to 1 destruction per scan.
|
||||
// Kill switch: env LEAKFIX_NO_SWEEP=1 disables.
|
||||
if (!g_sweep_armed) {
|
||||
// arm on second consecutive scan with candidates
|
||||
static int warmup_scans = 0;
|
||||
if (n_strict > 0) ++warmup_scans;
|
||||
if (warmup_scans >= 2) {
|
||||
g_sweep_armed = true;
|
||||
leakfix::logf("SWEEP ARMED — next scan will destroy 1 candidate per cycle");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
char no_sweep[8] = {0};
|
||||
GetEnvironmentVariableA("LEAKFIX_NO_SWEEP", no_sweep, sizeof(no_sweep));
|
||||
if (no_sweep[0] == '1') {
|
||||
leakfix::logf("LEAKFIX_NO_SWEEP=1 — destruction skipped this cycle");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!first_candidate) {
|
||||
leakfix::logf("sweep: no candidate this cycle");
|
||||
return;
|
||||
}
|
||||
|
||||
leakfix::logf("SWEEP destroying CPhysicsObj @ 0x%p", (void*)first_candidate);
|
||||
__try {
|
||||
physobj_destroy_fn destroy = (physobj_destroy_fn)CPHYSICSOBJ_DESTROY_VA;
|
||||
destroy((void*)first_candidate, 0);
|
||||
// After Destroy, the object's owned state should be cleaned up.
|
||||
// We do NOT free the CPhysicsObj itself yet — Destroy may already
|
||||
// mark it dead and downstream code might still need to deref a
|
||||
// few fields (vtable, etc.). Conservative.
|
||||
++g_total_destroyed;
|
||||
leakfix::logf("SWEEP ok — total destroyed=%lu", g_total_destroyed);
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
||||
leakfix::logf("SWEEP exception inside Destroy(); abandoning sweep this cycle. "
|
||||
"Set LEAKFIX_NO_SWEEP=1 in env to disable.");
|
||||
}
|
||||
}
|
||||
|
||||
void scan_once() {
|
||||
int counts[VT_COUNT] = {0};
|
||||
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
uintptr_t addr = 0;
|
||||
int region_count = 0;
|
||||
while (VirtualQuery((LPCVOID)addr, &mbi, sizeof(mbi))) {
|
||||
bool committed = mbi.State == MEM_COMMIT;
|
||||
bool priv = mbi.Type == MEM_PRIVATE;
|
||||
DWORD prot = mbi.Protect & 0xFF;
|
||||
bool readable = (prot == PAGE_READWRITE || prot == PAGE_EXECUTE_READWRITE);
|
||||
if (committed && priv && readable) {
|
||||
++region_count;
|
||||
// Scan in DWORD steps. In-process so direct deref is fine, but
|
||||
// wrap with SEH in case of races / partial commits.
|
||||
__try {
|
||||
const uint32_t* p = (const uint32_t*)mbi.BaseAddress;
|
||||
size_t n = mbi.RegionSize / 4;
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
uint32_t v = p[i];
|
||||
for (size_t k = 0; k < VT_COUNT; ++k) {
|
||||
if (v == VTABLES[k].vt) { ++counts[k]; break; }
|
||||
}
|
||||
}
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
||||
// skip this region — happens occasionally on volatile pages
|
||||
}
|
||||
}
|
||||
uintptr_t next = (uintptr_t)mbi.BaseAddress + mbi.RegionSize;
|
||||
if (next <= addr) break;
|
||||
addr = next;
|
||||
if (addr >= 0x80000000) break; // 32-bit user-space ceiling
|
||||
}
|
||||
|
||||
++g_scan_count;
|
||||
|
||||
char buf[1024];
|
||||
int n = std::snprintf(buf, sizeof(buf), "scan#%lu: regions=%d", g_scan_count, region_count);
|
||||
for (size_t k = 0; k < VT_COUNT && n < (int)sizeof(buf) - 16; ++k) {
|
||||
n += std::snprintf(buf + n, sizeof(buf) - n, " %s=%d", VTABLES[k].name, counts[k]);
|
||||
}
|
||||
leakfix::logf("%s", buf);
|
||||
|
||||
// Deltas vs previous scan (5-min interval after first scan)
|
||||
if (g_have_prev) {
|
||||
char dbuf[1024];
|
||||
int dn = std::snprintf(dbuf, sizeof(dbuf), "delta:");
|
||||
bool any_nonzero = false;
|
||||
for (size_t k = 0; k < VT_COUNT && dn < (int)sizeof(dbuf) - 32; ++k) {
|
||||
int d = counts[k] - g_prev_counts[k];
|
||||
if (d != 0) any_nonzero = true;
|
||||
dn += std::snprintf(dbuf + dn, sizeof(dbuf) - dn, " %s=%+d", VTABLES[k].name, d);
|
||||
}
|
||||
if (any_nonzero) leakfix::logf("%s", dbuf);
|
||||
}
|
||||
for (size_t k = 0; k < VT_COUNT; ++k) g_prev_counts[k] = counts[k];
|
||||
g_have_prev = true;
|
||||
|
||||
// Useful ratios (sanity-check our structural understanding):
|
||||
// position / cphysicsobj should be near 10 for active clients per v17
|
||||
// diag (each CPhysicsPart has 2 Positions; ~5 parts per physobj)
|
||||
// cphysicsobj count is what the sweep would target if/when we add it
|
||||
if (counts[2] > 0) { // cphysicsobj index = 2
|
||||
double pos_ratio = (double)counts[10] / (double)counts[2]; // position
|
||||
leakfix::logf("ratio: position/cphysicsobj=%.2f (idle ~7, active ~10)", pos_ratio);
|
||||
}
|
||||
|
||||
// ITER 2 — sample CPhysicsObj field layouts (every-other scan).
|
||||
if (counts[2] > 0 && (g_scan_count % 2 == 1)) {
|
||||
uintptr_t samples[CPHYS_SAMPLE_COUNT] = {0};
|
||||
int got = find_physobj_samples(samples, CPHYS_SAMPLE_COUNT);
|
||||
leakfix::logf("physobj-samples: got=%d of %d", got, CPHYS_SAMPLE_COUNT);
|
||||
for (int i = 0; i < got; ++i) dump_physobj(samples[i]);
|
||||
}
|
||||
|
||||
// ITER 3 — predicate evaluation across ALL CPhysicsObj instances
|
||||
// (every scan; full walk reused from scan_once iteration is too
|
||||
// costly so we do a second pass dedicated to this).
|
||||
if (counts[2] > 0) {
|
||||
evaluate_predicates_and_dump_candidates();
|
||||
}
|
||||
|
||||
// ITER 4 — strict predicates + (after 2 warmup scans) destruction.
|
||||
if (counts[2] > 0) {
|
||||
evaluate_strict_and_optionally_destroy();
|
||||
}
|
||||
}
|
||||
|
||||
DWORD WINAPI scan_loop(LPVOID) {
|
||||
// First scan ~30s after start so the process is warmed up.
|
||||
if (WaitForSingleObject(g_stop_event, 30000) == WAIT_OBJECT_0) return 0;
|
||||
for (;;) {
|
||||
scan_once();
|
||||
// Scan every 5 minutes thereafter.
|
||||
if (WaitForSingleObject(g_stop_event, 5 * 60 * 1000) == WAIT_OBJECT_0) return 0;
|
||||
}
|
||||
}
|
||||
|
||||
} // anon
|
||||
|
||||
|
||||
namespace leakfix {
|
||||
|
||||
void instr_install_crash_handler() {
|
||||
g_prev_filter = SetUnhandledExceptionFilter(top_level_handler);
|
||||
logf("instr: crash handler installed");
|
||||
}
|
||||
|
||||
void instr_start_periodic_scan() {
|
||||
if (g_scan_thread) return;
|
||||
g_stop_event = CreateEventA(nullptr, TRUE, FALSE, nullptr);
|
||||
g_scan_thread = CreateThread(nullptr, 0, scan_loop, nullptr, 0, nullptr);
|
||||
logf("instr: periodic scanner started (interval=5min)");
|
||||
}
|
||||
|
||||
void instr_stop_periodic_scan() {
|
||||
if (!g_scan_thread) return;
|
||||
if (g_stop_event) SetEvent(g_stop_event);
|
||||
WaitForSingleObject(g_scan_thread, 5000);
|
||||
CloseHandle(g_scan_thread);
|
||||
if (g_stop_event) CloseHandle(g_stop_event);
|
||||
g_scan_thread = nullptr;
|
||||
g_stop_event = nullptr;
|
||||
}
|
||||
|
||||
} // namespace leakfix
|
||||
Loading…
Add table
Add a link
Reference in a new issue