mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 17:43:52 +00:00
Fix Warden emulator heap leak and add analysis report
This commit is contained in:
parent
6260ac281e
commit
0554a01b39
3 changed files with 118 additions and 2 deletions
47
docs/memory_leak_report.md
Normal file
47
docs/memory_leak_report.md
Normal file
|
|
@ -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.
|
||||||
|
|
||||||
|
|
@ -152,6 +152,7 @@ private:
|
||||||
|
|
||||||
// Memory allocation tracking
|
// Memory allocation tracking
|
||||||
std::map<uint32_t, size_t> allocations_;
|
std::map<uint32_t, size_t> allocations_;
|
||||||
|
std::map<uint32_t, size_t> freeBlocks_; // Free-list blocks keyed by base address
|
||||||
uint32_t nextHeapAddr_;
|
uint32_t nextHeapAddr_;
|
||||||
|
|
||||||
// Hook handles for cleanup
|
// Hook handles for cleanup
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
#include "core/logger.hpp"
|
#include "core/logger.hpp"
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <iterator>
|
||||||
|
|
||||||
#ifdef HAVE_UNICORN
|
#ifdef HAVE_UNICORN
|
||||||
// Unicorn Engine headers
|
// Unicorn Engine headers
|
||||||
|
|
@ -46,6 +47,11 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3
|
||||||
LOG_ERROR("WardenEmulator: Already initialized");
|
LOG_ERROR("WardenEmulator: Already initialized");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
allocations_.clear();
|
||||||
|
freeBlocks_.clear();
|
||||||
|
apiAddresses_.clear();
|
||||||
|
hooks_.clear();
|
||||||
|
nextHeapAddr_ = heapBase_;
|
||||||
|
|
||||||
{
|
{
|
||||||
char addrBuf[32];
|
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) {
|
uint32_t WardenEmulator::allocateMemory(size_t size, [[maybe_unused]] uint32_t protection) {
|
||||||
|
if (size == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Align to 4KB
|
// Align to 4KB
|
||||||
size = (size + 0xFFF) & ~0xFFF;
|
size = (size + 0xFFF) & ~0xFFF;
|
||||||
|
const uint32_t allocSize = static_cast<uint32_t>(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<uint64_t>(heapBase_) + heapSize_;
|
||||||
|
if (static_cast<uint64_t>(nextHeapAddr_) + size > heapEnd) {
|
||||||
LOG_ERROR("WardenEmulator: Heap exhausted");
|
LOG_ERROR("WardenEmulator: Heap exhausted");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t addr = nextHeapAddr_;
|
uint32_t addr = nextHeapAddr_;
|
||||||
nextHeapAddr_ += size;
|
nextHeapAddr_ += allocSize;
|
||||||
|
|
||||||
allocations_[addr] = size;
|
allocations_[addr] = size;
|
||||||
|
|
||||||
|
|
@ -320,7 +352,43 @@ bool WardenEmulator::freeMemory(uint32_t address) {
|
||||||
std::snprintf(fBuf, sizeof(fBuf), "0x%X", address);
|
std::snprintf(fBuf, sizeof(fBuf), "0x%X", address);
|
||||||
LOG_DEBUG("WardenEmulator: Freed ", it->second, " bytes at ", fBuf);
|
LOG_DEBUG("WardenEmulator: Freed ", it->second, " bytes at ", fBuf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const size_t freedSize = it->second;
|
||||||
allocations_.erase(it);
|
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<uint64_t>(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<uint64_t>(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<uint64_t>(last->first) + last->second == nextHeapAddr_) {
|
||||||
|
nextHeapAddr_ = last->first;
|
||||||
|
freeBlocks_.erase(last);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue