mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-03 08:03:50 +00:00
memory, threading, network hardening
Signed-off-by: Pavel Okhlopkov <pavel.okhlopkov@flant.com>
This commit is contained in:
parent
312994be83
commit
2e8856bacd
9 changed files with 135 additions and 24 deletions
|
|
@ -48,6 +48,10 @@ public:
|
||||||
// Get session key (K) - used for encryption
|
// Get session key (K) - used for encryption
|
||||||
std::vector<uint8_t> getSessionKey() const;
|
std::vector<uint8_t> getSessionKey() const;
|
||||||
|
|
||||||
|
// Securely erase stored plaintext credentials from memory.
|
||||||
|
// Called automatically at the end of feed() once the SRP values are computed.
|
||||||
|
void clearCredentials();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// WoW-specific SRP multiplier (k = 3)
|
// WoW-specific SRP multiplier (k = 3)
|
||||||
static constexpr uint32_t K_VALUE = 3;
|
static constexpr uint32_t K_VALUE = 3;
|
||||||
|
|
|
||||||
|
|
@ -361,6 +361,8 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// MAIN-THREAD-ONLY: all entity map mutations happen via dispatchQueuedPackets()
|
||||||
|
// which runs on the main thread. Do NOT access from the async network pump thread.
|
||||||
std::unordered_map<uint64_t, std::shared_ptr<Entity>> entities;
|
std::unordered_map<uint64_t, std::shared_ptr<Entity>> entities;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -94,15 +94,18 @@ private:
|
||||||
void recordRecentPacket(bool outbound, uint16_t opcode, uint16_t payloadLen);
|
void recordRecentPacket(bool outbound, uint16_t opcode, uint16_t payloadLen);
|
||||||
void dumpRecentPacketHistoryLocked(const char* reason, size_t bufferedBytes);
|
void dumpRecentPacketHistoryLocked(const char* reason, size_t bufferedBytes);
|
||||||
|
|
||||||
socket_t sockfd = INVALID_SOCK;
|
socket_t sockfd = INVALID_SOCK; // THREAD-SAFE: protected by ioMutex_
|
||||||
bool connected = false;
|
bool connected = false; // THREAD-SAFE: protected by ioMutex_
|
||||||
bool encryptionEnabled = false;
|
bool encryptionEnabled = false; // THREAD-SAFE: protected by ioMutex_
|
||||||
bool useVanillaCrypt = false; // true = XOR cipher, false = RC4
|
bool useVanillaCrypt = false; // true = XOR cipher, false = RC4
|
||||||
bool useAsyncPump_ = true;
|
bool useAsyncPump_ = true;
|
||||||
std::thread asyncPumpThread_;
|
std::thread asyncPumpThread_;
|
||||||
std::atomic<bool> asyncPumpStop_{false};
|
std::atomic<bool> asyncPumpStop_{false}; // THREAD-SAFE: atomic
|
||||||
std::atomic<bool> asyncPumpRunning_{false};
|
std::atomic<bool> asyncPumpRunning_{false}; // THREAD-SAFE: atomic
|
||||||
|
// Guards sockfd, connected, encryptionEnabled, receiveBuffer, cipher state,
|
||||||
|
// headerBytesDecrypted, and recentPacketHistory_.
|
||||||
mutable std::mutex ioMutex_;
|
mutable std::mutex ioMutex_;
|
||||||
|
// Guards pendingPacketCallbacks_ (asyncPumpThread_ produces, main thread consumes).
|
||||||
mutable std::mutex callbackMutex_;
|
mutable std::mutex callbackMutex_;
|
||||||
|
|
||||||
// WotLK RC4 ciphers for header encryption/decryption
|
// WotLK RC4 ciphers for header encryption/decryption
|
||||||
|
|
@ -112,11 +115,12 @@ private:
|
||||||
// Vanilla/TBC XOR+addition cipher
|
// Vanilla/TBC XOR+addition cipher
|
||||||
auth::VanillaCrypt vanillaCrypt;
|
auth::VanillaCrypt vanillaCrypt;
|
||||||
|
|
||||||
// Receive buffer
|
// THREAD-SAFE: protected by ioMutex_
|
||||||
std::vector<uint8_t> receiveBuffer;
|
std::vector<uint8_t> receiveBuffer;
|
||||||
size_t receiveReadOffset_ = 0;
|
size_t receiveReadOffset_ = 0;
|
||||||
// Optional reused packet queue (feature-gated) to reduce per-update allocations.
|
// Optional reused packet queue (feature-gated) to reduce per-update allocations.
|
||||||
std::vector<Packet> parsedPacketsScratch_;
|
std::vector<Packet> parsedPacketsScratch_;
|
||||||
|
// THREAD-SAFE: protected by callbackMutex_.
|
||||||
// Parsed packets waiting for callback dispatch; drained with a strict per-update budget.
|
// Parsed packets waiting for callback dispatch; drained with a strict per-update budget.
|
||||||
std::deque<Packet> pendingPacketCallbacks_;
|
std::deque<Packet> pendingPacketCallbacks_;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
#include "pipeline/dbc_loader.hpp"
|
#include "pipeline/dbc_loader.hpp"
|
||||||
#include "pipeline/asset_manifest.hpp"
|
#include "pipeline/asset_manifest.hpp"
|
||||||
#include "pipeline/loose_file_reader.hpp"
|
#include "pipeline/loose_file_reader.hpp"
|
||||||
|
#include <atomic>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
@ -166,7 +167,11 @@ private:
|
||||||
*/
|
*/
|
||||||
std::string resolveFile(const std::string& normalizedPath) const;
|
std::string resolveFile(const std::string& normalizedPath) const;
|
||||||
|
|
||||||
|
// Guards fileCache, dbcCache, fileCacheTotalBytes, fileCacheAccessCounter, and
|
||||||
|
// fileCacheBudget. Shared lock for read-only cache lookups (readFile cache hit,
|
||||||
|
// loadDBC cache hit); exclusive lock for inserts and eviction.
|
||||||
mutable std::shared_mutex cacheMutex;
|
mutable std::shared_mutex cacheMutex;
|
||||||
|
// THREAD-SAFE: protected by cacheMutex (exclusive lock for writes).
|
||||||
std::unordered_map<std::string, std::shared_ptr<DBCFile>> dbcCache;
|
std::unordered_map<std::string, std::shared_ptr<DBCFile>> dbcCache;
|
||||||
|
|
||||||
// File cache (LRU, dynamic budget based on system RAM)
|
// File cache (LRU, dynamic budget based on system RAM)
|
||||||
|
|
@ -174,11 +179,14 @@ private:
|
||||||
std::vector<uint8_t> data;
|
std::vector<uint8_t> data;
|
||||||
uint64_t lastAccessTime;
|
uint64_t lastAccessTime;
|
||||||
};
|
};
|
||||||
|
// THREAD-SAFE: protected by cacheMutex (shared_mutex — shared_lock for reads,
|
||||||
|
// exclusive lock_guard for writes/eviction).
|
||||||
mutable std::unordered_map<std::string, CachedFile> fileCache;
|
mutable std::unordered_map<std::string, CachedFile> fileCache;
|
||||||
mutable size_t fileCacheTotalBytes = 0;
|
mutable size_t fileCacheTotalBytes = 0;
|
||||||
mutable uint64_t fileCacheAccessCounter = 0;
|
mutable uint64_t fileCacheAccessCounter = 0;
|
||||||
mutable size_t fileCacheHits = 0;
|
// THREAD-SAFE: atomic — incremented from any thread after releasing cacheMutex.
|
||||||
mutable size_t fileCacheMisses = 0;
|
mutable std::atomic<size_t> fileCacheHits{0};
|
||||||
|
mutable std::atomic<size_t> fileCacheMisses{0};
|
||||||
mutable size_t fileCacheBudget = 1024 * 1024 * 1024; // Dynamic, starts at 1GB
|
mutable size_t fileCacheBudget = 1024 * 1024 * 1024; // Dynamic, starts at 1GB
|
||||||
|
|
||||||
void setupFileCacheBudget();
|
void setupFileCacheBudget();
|
||||||
|
|
|
||||||
|
|
@ -362,10 +362,13 @@ private:
|
||||||
// Background loading worker pool
|
// Background loading worker pool
|
||||||
std::vector<std::thread> workerThreads;
|
std::vector<std::thread> workerThreads;
|
||||||
int workerCount = 0;
|
int workerCount = 0;
|
||||||
|
// THREAD-SAFE: guards loadQueue, readyQueue, and pendingTiles.
|
||||||
|
// Workers wait on queueCV; main thread signals when new tiles are enqueued
|
||||||
|
// or when readyQueue drains below maxReadyQueueSize_.
|
||||||
std::mutex queueMutex;
|
std::mutex queueMutex;
|
||||||
std::condition_variable queueCV;
|
std::condition_variable queueCV;
|
||||||
std::deque<TileCoord> loadQueue;
|
std::deque<TileCoord> loadQueue; // THREAD-SAFE: protected by queueMutex
|
||||||
std::queue<std::shared_ptr<PendingTile>> readyQueue;
|
std::queue<std::shared_ptr<PendingTile>> readyQueue; // THREAD-SAFE: protected by queueMutex
|
||||||
// Maximum number of prepared-but-not-finalized tiles in readyQueue.
|
// Maximum number of prepared-but-not-finalized tiles in readyQueue.
|
||||||
// Each prepared tile can hold 100–500 MB of decoded textures in RAM.
|
// Each prepared tile can hold 100–500 MB of decoded textures in RAM.
|
||||||
// Workers sleep when this limit is reached, letting the main thread
|
// Workers sleep when this limit is reached, letting the main thread
|
||||||
|
|
@ -378,6 +381,7 @@ private:
|
||||||
size_t bytes = 0;
|
size_t bytes = 0;
|
||||||
std::list<TileCoord>::iterator lruIt;
|
std::list<TileCoord>::iterator lruIt;
|
||||||
};
|
};
|
||||||
|
// THREAD-SAFE: protected by tileCacheMutex_.
|
||||||
std::unordered_map<TileCoord, CachedTile, TileCoord::Hash> tileCache_;
|
std::unordered_map<TileCoord, CachedTile, TileCoord::Hash> tileCache_;
|
||||||
std::list<TileCoord> tileCacheLru_;
|
std::list<TileCoord> tileCacheLru_;
|
||||||
size_t tileCacheBytes_ = 0;
|
size_t tileCacheBytes_ = 0;
|
||||||
|
|
@ -391,8 +395,8 @@ private:
|
||||||
std::atomic<bool> workerRunning{false};
|
std::atomic<bool> workerRunning{false};
|
||||||
|
|
||||||
// Track tiles currently queued or being processed to avoid duplicates
|
// Track tiles currently queued or being processed to avoid duplicates
|
||||||
std::unordered_map<TileCoord, bool, TileCoord::Hash> pendingTiles;
|
std::unordered_map<TileCoord, bool, TileCoord::Hash> pendingTiles; // THREAD-SAFE: protected by queueMutex
|
||||||
std::unordered_set<std::string> missingAdtWarnings_;
|
std::unordered_set<std::string> missingAdtWarnings_; // THREAD-SAFE: protected by missingAdtWarningsMutex_
|
||||||
std::mutex missingAdtWarningsMutex_;
|
std::mutex missingAdtWarningsMutex_;
|
||||||
|
|
||||||
// Thread-safe set of M2 model IDs already uploaded to GPU
|
// Thread-safe set of M2 model IDs already uploaded to GPU
|
||||||
|
|
@ -405,10 +409,11 @@ private:
|
||||||
std::unordered_set<uint32_t> preparedWmoUniqueIds_;
|
std::unordered_set<uint32_t> preparedWmoUniqueIds_;
|
||||||
std::mutex preparedWmoUniqueIdsMutex_;
|
std::mutex preparedWmoUniqueIdsMutex_;
|
||||||
|
|
||||||
// Dedup set for doodad placements across tile boundaries
|
// MAIN-THREAD-ONLY: checked and modified in processReadyTiles() and unloadDistantTiles(),
|
||||||
|
// both of which run exclusively on the main thread.
|
||||||
std::unordered_set<uint32_t> placedDoodadIds;
|
std::unordered_set<uint32_t> placedDoodadIds;
|
||||||
|
|
||||||
// Dedup set for WMO placements across tile boundaries (prevents rendering Stormwind 16x)
|
// MAIN-THREAD-ONLY: same contract as placedDoodadIds.
|
||||||
std::unordered_set<uint32_t> placedWmoIds;
|
std::unordered_set<uint32_t> placedWmoIds;
|
||||||
|
|
||||||
// Tiles currently being incrementally finalized across frames
|
// Tiles currently being incrementally finalized across frames
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <iterator>
|
#include <iterator>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <shared_mutex>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
namespace wowee {
|
namespace wowee {
|
||||||
|
|
@ -26,6 +27,10 @@ struct DecodedWavCacheEntry {
|
||||||
};
|
};
|
||||||
|
|
||||||
static std::unordered_map<uint64_t, DecodedWavCacheEntry> gDecodedWavCache;
|
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) {
|
static uint64_t makeWavCacheKey(const std::vector<uint8_t>& wavData) {
|
||||||
// FNV-1a over the first 256 bytes + last 256 bytes + total size.
|
// FNV-1a over the first 256 bytes + last 256 bytes + total size.
|
||||||
|
|
@ -53,10 +58,15 @@ static bool decodeWavCached(const std::vector<uint8_t>& wavData, DecodedWavCache
|
||||||
if (wavData.empty()) return false;
|
if (wavData.empty()) return false;
|
||||||
|
|
||||||
const uint64_t key = makeWavCacheKey(wavData);
|
const uint64_t key = makeWavCacheKey(wavData);
|
||||||
|
|
||||||
|
// 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()) {
|
if (auto it = gDecodedWavCache.find(key); it != gDecodedWavCache.end()) {
|
||||||
out = it->second;
|
out = it->second;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ma_decoder decoder;
|
ma_decoder decoder;
|
||||||
ma_decoder_config decoderConfig = ma_decoder_config_init_default();
|
ma_decoder_config decoderConfig = ma_decoder_config_init_default();
|
||||||
|
|
@ -102,6 +112,14 @@ 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
|
// 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
|
// PCM data depending on file lengths; halving keeps memory bounded while retaining
|
||||||
// recently-heard sounds (footsteps, UI clicks, combat hits) for instant replay.
|
// recently-heard sounds (footsteps, UI clicks, combat hits) for instant replay.
|
||||||
|
// 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;
|
constexpr size_t kMaxCachedSounds = 256;
|
||||||
if (gDecodedWavCache.size() >= kMaxCachedSounds) {
|
if (gDecodedWavCache.size() >= kMaxCachedSounds) {
|
||||||
auto it = gDecodedWavCache.begin();
|
auto it = gDecodedWavCache.begin();
|
||||||
|
|
@ -109,6 +127,7 @@ static bool decodeWavCached(const std::vector<uint8_t>& wavData, DecodedWavCache
|
||||||
gDecodedWavCache.erase(gDecodedWavCache.begin(), it);
|
gDecodedWavCache.erase(gDecodedWavCache.begin(), it);
|
||||||
}
|
}
|
||||||
gDecodedWavCache.emplace(key, entry);
|
gDecodedWavCache.emplace(key, entry);
|
||||||
|
}
|
||||||
out = entry;
|
out = entry;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,26 @@ void AuthHandler::disconnect() {
|
||||||
socket->disconnect();
|
socket->disconnect();
|
||||||
socket.reset();
|
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);
|
setState(AuthState::DISCONNECTED);
|
||||||
LOG_INFO("Disconnected from auth server");
|
LOG_INFO("Disconnected from auth server");
|
||||||
}
|
}
|
||||||
|
|
@ -354,6 +374,16 @@ void AuthHandler::handleLogonProofResponse(network::Packet& packet) {
|
||||||
sessionKey = srp->getSessionKey();
|
sessionKey = srp->getSessionKey();
|
||||||
setState(AuthState::AUTHENTICATED);
|
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("========================================");
|
||||||
LOG_INFO(" AUTHENTICATION SUCCESSFUL!");
|
LOG_INFO(" AUTHENTICATION SUCCESSFUL!");
|
||||||
LOG_INFO("========================================");
|
LOG_INFO("========================================");
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,10 @@ void SRP::feed(const std::vector<uint8_t>& B_bytes,
|
||||||
// 5. Compute proofs (M1, M2)
|
// 5. Compute proofs (M1, M2)
|
||||||
computeProofs(stored_username);
|
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
|
// Log key values for debugging auth issues
|
||||||
auto hexStr = [](const std::vector<uint8_t>& v, size_t maxBytes = 8) -> std::string {
|
auto hexStr = [](const std::vector<uint8_t>& v, size_t maxBytes = 8) -> std::string {
|
||||||
std::ostringstream ss;
|
std::ostringstream ss;
|
||||||
|
|
@ -314,5 +318,26 @@ std::vector<uint8_t> SRP::getSessionKey() const {
|
||||||
return K;
|
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 auth
|
||||||
} // namespace wowee
|
} // namespace wowee
|
||||||
|
|
|
||||||
|
|
@ -571,7 +571,21 @@ void WorldSocket::pumpNetworkIO() {
|
||||||
}
|
}
|
||||||
receiveBuffer.insert(receiveBuffer.end(), buffer, buffer + receivedSize);
|
receiveBuffer.insert(receiveBuffer.end(), buffer, buffer + receivedSize);
|
||||||
} else {
|
} 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) {
|
if (bufferedBytes() > kMaxReceiveBufferBytes) {
|
||||||
LOG_ERROR("World socket receive buffer overflow (", bufferedBytes(),
|
LOG_ERROR("World socket receive buffer overflow (", bufferedBytes(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue