From 0554a01b39d62f9799b9e7fb2b2ba5bb3547dd52 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 17 Mar 2026 21:40:15 +0100 Subject: [PATCH] Fix Warden emulator heap leak and add analysis report --- docs/memory_leak_report.md | 47 +++++++++++++++++++++ include/game/warden_emulator.hpp | 1 + src/game/warden_emulator.cpp | 72 +++++++++++++++++++++++++++++++- 3 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 docs/memory_leak_report.md diff --git a/docs/memory_leak_report.md b/docs/memory_leak_report.md new file mode 100644 index 00000000..5b7efe8a --- /dev/null +++ b/docs/memory_leak_report.md @@ -0,0 +1,47 @@ +# Memory Leak Analysis Report + +Date: 2026-03-17 +Repository: WoWee + +## Scope + +- Reviewed explicit heap allocation sites in `src/` and `include/`. +- Traced allocation/free paths for: + - `new` / `delete` + - `malloc` / `free` + - Warden emulator virtual heap allocation +- Focus was on leak behavior during long-running sessions. + +## Finding + +### 1) Warden emulator heap growth leak (fixed) + +- Location: `src/game/warden_emulator.cpp` +- Root cause: + - `allocateMemory()` used a bump pointer (`nextHeapAddr_`) only. + - `freeMemory()` removed entries from `allocations_`, but freed ranges were never reused. + - Result: repeated `VirtualAlloc`/`VirtualFree` patterns still advanced heap head until exhaustion. + +### Impact + +- Emulated heap (`HEAP_SIZE = 16MB`) could exhaust over time despite correct frees. +- This can break Warden module execution paths in extended sessions. + +## Patch Summary + +- Added a free-list map in `WardenEmulator` to track reusable blocks. +- Updated allocator to use first-fit from free-list before bump allocation. +- Added adjacent block coalescing in `freeMemory()`. +- Added top-of-heap rollback when highest free block touches current bump pointer. +- Reset allocator state on `initialize()` to avoid stale state across reinitialization. + +## Changed Files + +- `include/game/warden_emulator.hpp` +- `src/game/warden_emulator.cpp` + +## Notes + +- This was a static code analysis pass (no full runtime sanitizer execution in this environment). +- No other definite leaks were confirmed in this pass. + diff --git a/include/game/warden_emulator.hpp b/include/game/warden_emulator.hpp index 320afd0d..44739e93 100644 --- a/include/game/warden_emulator.hpp +++ b/include/game/warden_emulator.hpp @@ -152,6 +152,7 @@ private: // Memory allocation tracking std::map allocations_; + std::map freeBlocks_; // Free-list blocks keyed by base address uint32_t nextHeapAddr_; // Hook handles for cleanup diff --git a/src/game/warden_emulator.cpp b/src/game/warden_emulator.cpp index 9338cc00..583d1d6f 100644 --- a/src/game/warden_emulator.cpp +++ b/src/game/warden_emulator.cpp @@ -2,6 +2,7 @@ #include "core/logger.hpp" #include #include +#include #ifdef HAVE_UNICORN // Unicorn Engine headers @@ -46,6 +47,11 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3 LOG_ERROR("WardenEmulator: Already initialized"); return false; } + allocations_.clear(); + freeBlocks_.clear(); + apiAddresses_.clear(); + hooks_.clear(); + nextHeapAddr_ = heapBase_; { char addrBuf[32]; @@ -282,16 +288,42 @@ std::string WardenEmulator::readString(uint32_t address, size_t maxLen) { } uint32_t WardenEmulator::allocateMemory(size_t size, [[maybe_unused]] uint32_t protection) { + if (size == 0) { + return 0; + } + // Align to 4KB size = (size + 0xFFF) & ~0xFFF; + const uint32_t allocSize = static_cast(size); - if (nextHeapAddr_ + size > heapBase_ + heapSize_) { + // First-fit from free list so released blocks can be reused. + for (auto it = freeBlocks_.begin(); it != freeBlocks_.end(); ++it) { + if (it->second < size) { + continue; + } + const uint32_t addr = it->first; + const size_t blockSize = it->second; + freeBlocks_.erase(it); + if (blockSize > size) { + freeBlocks_[addr + allocSize] = blockSize - size; + } + allocations_[addr] = size; + { + char mBuf[32]; + std::snprintf(mBuf, sizeof(mBuf), "0x%X", addr); + LOG_DEBUG("WardenEmulator: Reused ", size, " bytes at ", mBuf); + } + return addr; + } + + const uint64_t heapEnd = static_cast(heapBase_) + heapSize_; + if (static_cast(nextHeapAddr_) + size > heapEnd) { LOG_ERROR("WardenEmulator: Heap exhausted"); return 0; } uint32_t addr = nextHeapAddr_; - nextHeapAddr_ += size; + nextHeapAddr_ += allocSize; allocations_[addr] = size; @@ -320,7 +352,43 @@ bool WardenEmulator::freeMemory(uint32_t address) { std::snprintf(fBuf, sizeof(fBuf), "0x%X", address); LOG_DEBUG("WardenEmulator: Freed ", it->second, " bytes at ", fBuf); } + + const size_t freedSize = it->second; allocations_.erase(it); + + // Insert in free list and coalesce adjacent blocks to limit fragmentation. + auto [curr, inserted] = freeBlocks_.emplace(address, freedSize); + if (!inserted) { + curr->second += freedSize; + } + + if (curr != freeBlocks_.begin()) { + auto prev = std::prev(curr); + if (static_cast(prev->first) + prev->second == curr->first) { + prev->second += curr->second; + freeBlocks_.erase(curr); + curr = prev; + } + } + + auto next = std::next(curr); + if (next != freeBlocks_.end() && + static_cast(curr->first) + curr->second == next->first) { + curr->second += next->second; + freeBlocks_.erase(next); + } + + // If the highest free block reaches the bump pointer, roll back the heap top. + while (!freeBlocks_.empty()) { + auto last = std::prev(freeBlocks_.end()); + if (static_cast(last->first) + last->second == nextHeapAddr_) { + nextHeapAddr_ = last->first; + freeBlocks_.erase(last); + } else { + break; + } + } + return true; }