Merge pull request #54 from ldmonster/fix/memory-pressure-and-hardening

[fix] Memory, Threading & Network Hardening
This commit is contained in:
Kelsi Rae Davis 2026-04-06 13:45:31 -07:00 committed by GitHub
commit 20e016798f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 333 additions and 25 deletions

View file

@ -10,6 +10,7 @@
#include <cstdlib>
#include <iterator>
#include <memory>
#include <shared_mutex>
#include <unordered_map>
namespace wowee {
@ -26,6 +27,10 @@ struct DecodedWavCacheEntry {
};
static std::unordered_map<uint64_t, DecodedWavCacheEntry> gDecodedWavCache;
// Protects gDecodedWavCache — shared_lock for reads, unique_lock for writes.
// Required because playSound2D() can be called from multiple threads
// (main thread, async loaders, animation callbacks).
static std::shared_mutex gDecodedWavCacheMutex;
static uint64_t makeWavCacheKey(const std::vector<uint8_t>& wavData) {
// FNV-1a over the first 256 bytes + last 256 bytes + total size.
@ -53,9 +58,14 @@ static bool decodeWavCached(const std::vector<uint8_t>& wavData, DecodedWavCache
if (wavData.empty()) return false;
const uint64_t key = makeWavCacheKey(wavData);
if (auto it = gDecodedWavCache.find(key); it != gDecodedWavCache.end()) {
out = it->second;
return true;
// Fast path: shared (read) lock for cache hits — allows concurrent lookups.
{
std::shared_lock<std::shared_mutex> readLock(gDecodedWavCacheMutex);
if (auto it = gDecodedWavCache.find(key); it != gDecodedWavCache.end()) {
out = it->second;
return true;
}
}
ma_decoder decoder;
@ -102,13 +112,22 @@ static bool decodeWavCached(const std::vector<uint8_t>& wavData, DecodedWavCache
// Evict oldest half when cache grows too large. 256 entries ≈ 50-100 MB of decoded
// PCM data depending on file lengths; halving keeps memory bounded while retaining
// recently-heard sounds (footsteps, UI clicks, combat hits) for instant replay.
constexpr size_t kMaxCachedSounds = 256;
if (gDecodedWavCache.size() >= kMaxCachedSounds) {
auto it = gDecodedWavCache.begin();
std::advance(it, gDecodedWavCache.size() / 2);
gDecodedWavCache.erase(gDecodedWavCache.begin(), it);
// Exclusive (write) lock — only one thread can evict + insert.
{
std::lock_guard<std::shared_mutex> writeLock(gDecodedWavCacheMutex);
// Re-check in case another thread inserted while we were decoding.
if (auto it = gDecodedWavCache.find(key); it != gDecodedWavCache.end()) {
out = it->second;
return true;
}
constexpr size_t kMaxCachedSounds = 256;
if (gDecodedWavCache.size() >= kMaxCachedSounds) {
auto it = gDecodedWavCache.begin();
std::advance(it, gDecodedWavCache.size() / 2);
gDecodedWavCache.erase(gDecodedWavCache.begin(), it);
}
gDecodedWavCache.emplace(key, entry);
}
gDecodedWavCache.emplace(key, entry);
out = entry;
return true;
}

View file

@ -68,6 +68,26 @@ void AuthHandler::disconnect() {
socket->disconnect();
socket.reset();
}
// Scrub sensitive material when tearing down the auth session.
if (!password.empty()) {
volatile char* p = const_cast<volatile char*>(password.data());
for (size_t i = 0; i < password.size(); ++i)
p[i] = '\0';
password.clear();
password.shrink_to_fit();
}
if (!sessionKey.empty()) {
volatile uint8_t* k = const_cast<volatile uint8_t*>(sessionKey.data());
for (size_t i = 0; i < sessionKey.size(); ++i)
k[i] = 0;
sessionKey.clear();
sessionKey.shrink_to_fit();
}
if (srp) {
srp->clearCredentials();
}
setState(AuthState::DISCONNECTED);
LOG_INFO("Disconnected from auth server");
}
@ -354,6 +374,16 @@ void AuthHandler::handleLogonProofResponse(network::Packet& packet) {
sessionKey = srp->getSessionKey();
setState(AuthState::AUTHENTICATED);
// Plaintext password is no longer needed — zero-fill and release it so it
// doesn't sit in process memory for the rest of the session.
if (!password.empty()) {
volatile char* p = const_cast<volatile char*>(password.data());
for (size_t i = 0; i < password.size(); ++i)
p[i] = '\0';
password.clear();
password.shrink_to_fit();
}
LOG_INFO("========================================");
LOG_INFO(" AUTHENTICATION SUCCESSFUL!");
LOG_INFO("========================================");

View file

@ -96,6 +96,10 @@ void SRP::feed(const std::vector<uint8_t>& B_bytes,
// 5. Compute proofs (M1, M2)
computeProofs(stored_username);
// Credentials are no longer needed — zero and release them so they don't
// linger in process memory longer than necessary.
clearCredentials();
// Log key values for debugging auth issues
auto hexStr = [](const std::vector<uint8_t>& v, size_t maxBytes = 8) -> std::string {
std::ostringstream ss;
@ -314,5 +318,26 @@ std::vector<uint8_t> SRP::getSessionKey() const {
return K;
}
void SRP::clearCredentials() {
// Overwrite plaintext password bytes before releasing storage so that a
// heap dump / core file doesn't leak the user's credentials. This is
// not a guarantee against a privileged attacker with live memory access,
// but it removes the most common exposure vector.
if (!stored_password.empty()) {
volatile char* p = const_cast<volatile char*>(stored_password.data());
for (size_t i = 0; i < stored_password.size(); ++i)
p[i] = '\0';
stored_password.clear();
stored_password.shrink_to_fit();
}
if (!stored_auth_hash.empty()) {
volatile uint8_t* h = const_cast<volatile uint8_t*>(stored_auth_hash.data());
for (size_t i = 0; i < stored_auth_hash.size(); ++i)
h[i] = 0;
stored_auth_hash.clear();
stored_auth_hash.shrink_to_fit();
}
}
} // namespace auth
} // namespace wowee

View file

@ -126,5 +126,12 @@ bool MemoryMonitor::isMemoryPressure() const {
return available < (totalRAM_ * 10 / 100);
}
bool MemoryMonitor::isSevereMemoryPressure() const {
size_t available = getAvailableRAM();
// Severe pressure if < 15% RAM available — background workers should
// pause entirely to avoid OOM-killing other applications.
return available < (totalRAM_ * 15 / 100);
}
} // namespace core
} // namespace wowee

View file

@ -571,7 +571,21 @@ void WorldSocket::pumpNetworkIO() {
}
receiveBuffer.insert(receiveBuffer.end(), buffer, buffer + receivedSize);
} else {
receiveBuffer.insert(receiveBuffer.end(), buffer, buffer + received);
// Non-fast path: same overflow pre-check as fast path to prevent
// unbounded buffer growth before the post-check below.
size_t liveBytes = bufferedBytes();
if (liveBytes > kMaxReceiveBufferBytes || receivedSize > (kMaxReceiveBufferBytes - liveBytes)) {
compactReceiveBuffer();
liveBytes = bufferedBytes();
}
if (liveBytes > kMaxReceiveBufferBytes || receivedSize > (kMaxReceiveBufferBytes - liveBytes)) {
LOG_ERROR("World socket receive buffer would overflow (buffered=", liveBytes,
" incoming=", receivedSize, " max=", kMaxReceiveBufferBytes,
"). Disconnecting to recover framing.");
closeSocketNoJoin();
return;
}
receiveBuffer.insert(receiveBuffer.end(), buffer, buffer + receivedSize);
}
if (bufferedBytes() > kMaxReceiveBufferBytes) {
LOG_ERROR("World socket receive buffer overflow (", bufferedBytes(),

View file

@ -1212,6 +1212,28 @@ void TerrainManager::workerLoop() {
break;
}
// --- Memory-aware throttling ---
// Back-pressure: if the ready queue is deep (finalization can't
// keep up), or the system is running low on RAM, sleep instead
// of pulling more tiles. Each prepared tile can hold hundreds
// of MB of decoded textures; limiting concurrency here prevents
// WoWee from consuming all system memory during world load.
const auto& memMon = core::MemoryMonitor::getInstance();
if (memMon.isSevereMemoryPressure()) {
// Severe pressure — don't pull ANY work until main thread
// finalizes tiles and frees decoded texture data.
lock.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(200));
continue;
}
if (readyQueue.size() >= maxReadyQueueSize_ || memMon.isMemoryPressure()) {
// Moderate pressure or ready queue is backing up — sleep briefly
// to let the main thread catch up with finalization.
lock.unlock();
std::this_thread::sleep_for(std::chrono::milliseconds(50));
continue;
}
if (!loadQueue.empty()) {
coord = loadQueue.front();
loadQueue.pop_front();