From 5756826723b690a8006797e031f3273fd4f196f5 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 09:29:02 -0700 Subject: [PATCH 01/31] fix(combatlog): preserve unknown source for environmental entries --- src/game/game_handler.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 16374768..138e3589 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -14381,8 +14381,11 @@ void GameHandler::addCombatText(CombatTextEntry::Type type, int32_t amount, uint log.spellId = spellId; log.isPlayerSource = isPlayerSource; log.timestamp = std::time(nullptr); + // If the caller provided an explicit destination GUID but left source GUID as 0, + // preserve "unknown/no source" (e.g. environmental damage) instead of + // backfilling from current target. uint64_t effectiveSrc = (srcGuid != 0) ? srcGuid - : (isPlayerSource ? playerGuid : targetGuid); + : ((dstGuid != 0) ? 0 : (isPlayerSource ? playerGuid : targetGuid)); uint64_t effectiveDst = (dstGuid != 0) ? dstGuid : (isPlayerSource ? targetGuid : playerGuid); log.sourceName = lookupName(effectiveSrc); From 6d06da52be76dfeb7111943b482ed1383340a89a Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 09:36:42 -0700 Subject: [PATCH 02/31] fix(combatlog): show resisted amount from resist log packets --- src/game/game_handler.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 138e3589..1774c003 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -7105,11 +7105,20 @@ void GameHandler::handlePacket(network::Packet& packet) { ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); if (rl_rem() < 4) { packet.setReadPos(packet.getSize()); break; } uint32_t spellId = packet.readUInt32(); + int32_t resistedAmount = 0; + // Resist payload includes: + // float resistFactor + uint32 targetResistance + uint32 resistedValue. + // Some servers may truncate optional tail fields, so parse defensively. + if (rl_rem() >= 12) { + /*float resistFactor =*/ packet.readFloat(); + /*uint32_t targetRes =*/ packet.readUInt32(); + resistedAmount = static_cast(packet.readUInt32()); + } // Show RESIST when the player is involved on either side. if (victimGuid == playerGuid) { - addCombatText(CombatTextEntry::RESIST, 0, spellId, false, 0, attackerGuid, victimGuid); + addCombatText(CombatTextEntry::RESIST, resistedAmount, spellId, false, 0, attackerGuid, victimGuid); } else if (attackerGuid == playerGuid) { - addCombatText(CombatTextEntry::RESIST, 0, spellId, true, 0, attackerGuid, victimGuid); + addCombatText(CombatTextEntry::RESIST, resistedAmount, spellId, true, 0, attackerGuid, victimGuid); } packet.setReadPos(packet.getSize()); break; From 7f5dedd57e43079276a9610779adeb9472de919a Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 09:44:52 -0700 Subject: [PATCH 03/31] fix(combatlog): map immune2 spell miss results correctly --- src/game/game_handler.cpp | 45 +++++++++++++++++---------------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 1774c003..ba13a05f 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -116,6 +116,23 @@ bool hasFullPackedGuid(const network::Packet& packet) { return packet.getSize() - packet.getReadPos() >= guidBytes; } +CombatTextEntry::Type combatTextTypeFromSpellMissInfo(uint8_t missInfo) { + switch (missInfo) { + case 0: return CombatTextEntry::MISS; + case 1: return CombatTextEntry::DODGE; + case 2: return CombatTextEntry::PARRY; + case 3: return CombatTextEntry::BLOCK; + case 4: return CombatTextEntry::EVADE; + case 5: return CombatTextEntry::IMMUNE; + case 6: return CombatTextEntry::DEFLECT; + case 7: return CombatTextEntry::ABSORB; + case 8: return CombatTextEntry::RESIST; + case 10: return CombatTextEntry::IMMUNE; // Seen on some cores as a secondary immune result. + case 11: return CombatTextEntry::REFLECT; + default: return CombatTextEntry::MISS; + } +} + std::string formatCopperAmount(uint32_t amount) { uint32_t gold = amount / 10000; uint32_t silver = (amount / 100) % 100; @@ -2761,19 +2778,7 @@ void GameHandler::handlePacket(network::Packet& packet) { break; } } - static const CombatTextEntry::Type missTypes[] = { - CombatTextEntry::MISS, // 0=MISS - CombatTextEntry::DODGE, // 1=DODGE - CombatTextEntry::PARRY, // 2=PARRY - CombatTextEntry::BLOCK, // 3=BLOCK - CombatTextEntry::EVADE, // 4=EVADE - CombatTextEntry::IMMUNE, // 5=IMMUNE - CombatTextEntry::DEFLECT, // 6=DEFLECT - CombatTextEntry::ABSORB, // 7=ABSORB - CombatTextEntry::RESIST, // 8=RESIST - }; - CombatTextEntry::Type ct = (missInfo < 9) ? missTypes[missInfo] - : (missInfo == 11 ? CombatTextEntry::REFLECT : CombatTextEntry::MISS); + CombatTextEntry::Type ct = combatTextTypeFromSpellMissInfo(missInfo); if (casterGuid == playerGuid) { // We cast a spell and it missed the target addCombatText(ct, 0, spellId, true, 0, casterGuid, victimGuid); @@ -17236,17 +17241,6 @@ void GameHandler::handleSpellGo(network::Packet& packet) { // Preserve spellId and actual participants for spell-go miss results. // This keeps the persistent combat log aligned with the later GUID fixes. if (!data.missTargets.empty()) { - static const CombatTextEntry::Type missTypes[] = { - CombatTextEntry::MISS, // 0=MISS - CombatTextEntry::DODGE, // 1=DODGE - CombatTextEntry::PARRY, // 2=PARRY - CombatTextEntry::BLOCK, // 3=BLOCK - CombatTextEntry::EVADE, // 4=EVADE - CombatTextEntry::IMMUNE, // 5=IMMUNE - CombatTextEntry::DEFLECT, // 6=DEFLECT - CombatTextEntry::ABSORB, // 7=ABSORB - CombatTextEntry::RESIST, // 8=RESIST - }; const uint64_t spellCasterGuid = data.casterUnit != 0 ? data.casterUnit : data.casterGuid; const bool playerIsCaster = (spellCasterGuid == playerGuid); @@ -17254,8 +17248,7 @@ void GameHandler::handleSpellGo(network::Packet& packet) { if (!playerIsCaster && m.targetGuid != playerGuid) { continue; } - CombatTextEntry::Type ct = (m.missType < 9) ? missTypes[m.missType] - : (m.missType == 11 ? CombatTextEntry::REFLECT : CombatTextEntry::MISS); + CombatTextEntry::Type ct = combatTextTypeFromSpellMissInfo(m.missType); addCombatText(ct, 0, data.spellId, playerIsCaster, 0, spellCasterGuid, m.targetGuid); } } From 51cff764a98b9abf7d7071f57a88742b60c60312 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 09:52:33 -0700 Subject: [PATCH 04/31] fix(combatlog): map alternate immune2 spell miss value --- src/game/game_handler.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index ba13a05f..9cd0a385 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -127,7 +127,9 @@ CombatTextEntry::Type combatTextTypeFromSpellMissInfo(uint8_t missInfo) { case 6: return CombatTextEntry::DEFLECT; case 7: return CombatTextEntry::ABSORB; case 8: return CombatTextEntry::RESIST; - case 10: return CombatTextEntry::IMMUNE; // Seen on some cores as a secondary immune result. + case 9: // Some cores encode SPELL_MISS_IMMUNE2 as 9. + case 10: // Others encode SPELL_MISS_IMMUNE2 as 10. + return CombatTextEntry::IMMUNE; case 11: return CombatTextEntry::REFLECT; default: return CombatTextEntry::MISS; } From b8d694d6b378633eb5095155588ec38931696ace Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 10:00:45 -0700 Subject: [PATCH 05/31] fix(combatlog): parse full spell miss target lists --- src/game/game_handler.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 9cd0a385..fec1caaa 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2761,7 +2761,6 @@ void GameHandler::handlePacket(network::Packet& packet) { if (packet.getSize() - packet.getReadPos() < 5) break; /*uint8_t unk =*/ packet.readUInt8(); uint32_t count = packet.readUInt32(); - count = std::min(count, 32u); for (uint32_t i = 0; i < count; ++i) { if (packet.getSize() - packet.getReadPos() < (spellMissUsesFullGuid ? 9u : 2u) || (!spellMissUsesFullGuid && !hasFullPackedGuid(packet))) { From 0077986a220156db83f83afbd1d8ac53126f794a Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 10:09:04 -0700 Subject: [PATCH 06/31] fix(combatlog): consume full spell go target lists when capped --- src/game/packet_parsers_classic.cpp | 47 +++++++++++++++-------------- src/game/packet_parsers_tbc.cpp | 47 +++++++++++++++-------------- src/game/world_packets.cpp | 46 ++++++++++++++-------------- 3 files changed, 74 insertions(+), 66 deletions(-) diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index 046839d0..4528ef72 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -410,33 +410,34 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da // Hit targets if (rem() < 1) return true; - data.hitCount = packet.readUInt8(); - // Cap hit count to prevent OOM from huge target lists - if (data.hitCount > 128) { - LOG_WARNING("[Classic] Spell go: hitCount capped (requested=", (int)data.hitCount, ")"); - data.hitCount = 128; + const uint8_t rawHitCount = packet.readUInt8(); + if (rawHitCount > 128) { + LOG_WARNING("[Classic] Spell go: hitCount capped (requested=", (int)rawHitCount, ")"); } - data.hitTargets.reserve(data.hitCount); - for (uint8_t i = 0; i < data.hitCount && rem() >= 1; ++i) { - data.hitTargets.push_back(UpdateObjectParser::readPackedGuid(packet)); + const uint8_t storedHitLimit = std::min(rawHitCount, 128); + data.hitTargets.reserve(storedHitLimit); + for (uint16_t i = 0; i < rawHitCount && rem() >= 1; ++i) { + const uint64_t targetGuid = UpdateObjectParser::readPackedGuid(packet); + if (i < storedHitLimit) { + data.hitTargets.push_back(targetGuid); + } } + data.hitCount = static_cast(data.hitTargets.size()); // Check if we read all expected hits - if (data.hitTargets.size() < data.hitCount) { + if (data.hitTargets.size() < rawHitCount) { LOG_WARNING("[Classic] Spell go: truncated hit targets at index ", (int)data.hitTargets.size(), - "/", (int)data.hitCount); - data.hitCount = data.hitTargets.size(); + "/", (int)rawHitCount); } // Miss targets if (rem() < 1) return true; - data.missCount = packet.readUInt8(); - // Cap miss count to prevent OOM - if (data.missCount > 128) { - LOG_WARNING("[Classic] Spell go: missCount capped (requested=", (int)data.missCount, ")"); - data.missCount = 128; + const uint8_t rawMissCount = packet.readUInt8(); + if (rawMissCount > 128) { + LOG_WARNING("[Classic] Spell go: missCount capped (requested=", (int)rawMissCount, ")"); } - data.missTargets.reserve(data.missCount); - for (uint8_t i = 0; i < data.missCount && rem() >= 2; ++i) { + const uint8_t storedMissLimit = std::min(rawMissCount, 128); + data.missTargets.reserve(storedMissLimit); + for (uint16_t i = 0; i < rawMissCount && rem() >= 2; ++i) { SpellGoMissEntry m; m.targetGuid = UpdateObjectParser::readPackedGuid(packet); if (rem() < 1) break; @@ -446,13 +447,15 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da (void)packet.readUInt32(); (void)packet.readUInt8(); } - data.missTargets.push_back(m); + if (i < storedMissLimit) { + data.missTargets.push_back(m); + } } + data.missCount = static_cast(data.missTargets.size()); // Check if we read all expected misses - if (data.missTargets.size() < data.missCount) { + if (data.missTargets.size() < rawMissCount) { LOG_WARNING("[Classic] Spell go: truncated miss targets at index ", (int)data.missTargets.size(), - "/", (int)data.missCount); - data.missCount = data.missTargets.size(); + "/", (int)rawMissCount); } LOG_DEBUG("[Classic] Spell go: spell=", data.spellId, " hits=", (int)data.hitCount, diff --git a/src/game/packet_parsers_tbc.cpp b/src/game/packet_parsers_tbc.cpp index d218926a..ba2393c3 100644 --- a/src/game/packet_parsers_tbc.cpp +++ b/src/game/packet_parsers_tbc.cpp @@ -1277,32 +1277,33 @@ bool TbcPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) return true; } - data.hitCount = packet.readUInt8(); - // Cap hit count to prevent OOM from huge target lists - if (data.hitCount > 128) { - LOG_WARNING("[TBC] Spell go: hitCount capped (requested=", (int)data.hitCount, ")"); - data.hitCount = 128; + const uint8_t rawHitCount = packet.readUInt8(); + if (rawHitCount > 128) { + LOG_WARNING("[TBC] Spell go: hitCount capped (requested=", (int)rawHitCount, ")"); } - data.hitTargets.reserve(data.hitCount); - for (uint8_t i = 0; i < data.hitCount && packet.getReadPos() + 8 <= packet.getSize(); ++i) { - data.hitTargets.push_back(packet.readUInt64()); // full GUID in TBC + const uint8_t storedHitLimit = std::min(rawHitCount, 128); + data.hitTargets.reserve(storedHitLimit); + for (uint16_t i = 0; i < rawHitCount && packet.getReadPos() + 8 <= packet.getSize(); ++i) { + const uint64_t targetGuid = packet.readUInt64(); // full GUID in TBC + if (i < storedHitLimit) { + data.hitTargets.push_back(targetGuid); + } } + data.hitCount = static_cast(data.hitTargets.size()); // Check if we read all expected hits - if (data.hitTargets.size() < data.hitCount) { + if (data.hitTargets.size() < rawHitCount) { LOG_WARNING("[TBC] Spell go: truncated hit targets at index ", (int)data.hitTargets.size(), - "/", (int)data.hitCount); - data.hitCount = data.hitTargets.size(); + "/", (int)rawHitCount); } if (packet.getReadPos() < packet.getSize()) { - data.missCount = packet.readUInt8(); - // Cap miss count to prevent OOM - if (data.missCount > 128) { - LOG_WARNING("[TBC] Spell go: missCount capped (requested=", (int)data.missCount, ")"); - data.missCount = 128; + const uint8_t rawMissCount = packet.readUInt8(); + if (rawMissCount > 128) { + LOG_WARNING("[TBC] Spell go: missCount capped (requested=", (int)rawMissCount, ")"); } - data.missTargets.reserve(data.missCount); - for (uint8_t i = 0; i < data.missCount && packet.getReadPos() + 9 <= packet.getSize(); ++i) { + const uint8_t storedMissLimit = std::min(rawMissCount, 128); + data.missTargets.reserve(storedMissLimit); + for (uint16_t i = 0; i < rawMissCount && packet.getReadPos() + 9 <= packet.getSize(); ++i) { SpellGoMissEntry m; m.targetGuid = packet.readUInt64(); // full GUID in TBC m.missType = packet.readUInt8(); @@ -1313,13 +1314,15 @@ bool TbcPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) (void)packet.readUInt32(); (void)packet.readUInt8(); } - data.missTargets.push_back(m); + if (i < storedMissLimit) { + data.missTargets.push_back(m); + } } + data.missCount = static_cast(data.missTargets.size()); // Check if we read all expected misses - if (data.missTargets.size() < data.missCount) { + if (data.missTargets.size() < rawMissCount) { LOG_WARNING("[TBC] Spell go: truncated miss targets at index ", (int)data.missTargets.size(), - "/", (int)data.missCount); - data.missCount = data.missTargets.size(); + "/", (int)rawMissCount); } } diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 659003d7..686dba75 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -3714,43 +3714,43 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { // Timestamp in 3.3.5a packet.readUInt32(); - data.hitCount = packet.readUInt8(); - // Cap hit count to prevent DoS via massive arrays - if (data.hitCount > 128) { - LOG_WARNING("Spell go: hitCount capped (requested=", (int)data.hitCount, ")"); - data.hitCount = 128; + const uint8_t rawHitCount = packet.readUInt8(); + if (rawHitCount > 128) { + LOG_WARNING("Spell go: hitCount capped (requested=", (int)rawHitCount, ")"); } + const uint8_t storedHitLimit = std::min(rawHitCount, 128); - data.hitTargets.reserve(data.hitCount); - for (uint8_t i = 0; i < data.hitCount; ++i) { + data.hitTargets.reserve(storedHitLimit); + for (uint16_t i = 0; i < rawHitCount; ++i) { // WotLK hit targets are packed GUIDs, like the caster and miss targets. if (packet.getSize() - packet.getReadPos() < 1) { - LOG_WARNING("Spell go: truncated hit targets at index ", (int)i, "/", (int)data.hitCount); - data.hitCount = i; + LOG_WARNING("Spell go: truncated hit targets at index ", i, "/", (int)rawHitCount); break; } - data.hitTargets.push_back(UpdateObjectParser::readPackedGuid(packet)); + const uint64_t targetGuid = UpdateObjectParser::readPackedGuid(packet); + if (i < storedHitLimit) { + data.hitTargets.push_back(targetGuid); + } } + data.hitCount = static_cast(data.hitTargets.size()); // Validate missCount field exists if (packet.getSize() - packet.getReadPos() < 1) { return true; // Valid, just no misses } - data.missCount = packet.readUInt8(); - // Cap miss count to prevent DoS - if (data.missCount > 128) { - LOG_WARNING("Spell go: missCount capped (requested=", (int)data.missCount, ")"); - data.missCount = 128; + const uint8_t rawMissCount = packet.readUInt8(); + if (rawMissCount > 128) { + LOG_WARNING("Spell go: missCount capped (requested=", (int)rawMissCount, ")"); } + const uint8_t storedMissLimit = std::min(rawMissCount, 128); - data.missTargets.reserve(data.missCount); - for (uint8_t i = 0; i < data.missCount; ++i) { + data.missTargets.reserve(storedMissLimit); + for (uint16_t i = 0; i < rawMissCount; ++i) { // Each miss entry: packed GUID(1-8 bytes) + missType(1 byte). // REFLECT additionally appends uint32 reflectSpellId + uint8 reflectResult. if (packet.getSize() - packet.getReadPos() < 2) { - LOG_WARNING("Spell go: truncated miss targets at index ", (int)i, "/", (int)data.missCount); - data.missCount = i; + LOG_WARNING("Spell go: truncated miss targets at index ", i, "/", (int)rawMissCount); break; } SpellGoMissEntry m; @@ -3758,15 +3758,17 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { m.missType = (packet.getSize() - packet.getReadPos() >= 1) ? packet.readUInt8() : 0; if (m.missType == 11) { if (packet.getSize() - packet.getReadPos() < 5) { - LOG_WARNING("Spell go: truncated reflect payload at miss index ", (int)i, "/", (int)data.missCount); - data.missCount = i; + LOG_WARNING("Spell go: truncated reflect payload at miss index ", i, "/", (int)rawMissCount); break; } (void)packet.readUInt32(); (void)packet.readUInt8(); } - data.missTargets.push_back(m); + if (i < storedMissLimit) { + data.missTargets.push_back(m); + } } + data.missCount = static_cast(data.missTargets.size()); LOG_DEBUG("Spell go: spell=", data.spellId, " hits=", (int)data.hitCount, " misses=", (int)data.missCount); From bfe9167a4258c43a7f21fd8b09d1b9add847aab4 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 10:17:19 -0700 Subject: [PATCH 07/31] fix(combatlog): validate packed GUID bounds in spell cast parsers --- src/game/world_packets.cpp | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 686dba75..02c20a6c 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -19,6 +19,22 @@ namespace { inline uint16_t bswap16(uint16_t v) { return static_cast(((v & 0xFF00u) >> 8) | ((v & 0x00FFu) << 8)); } + + bool hasFullPackedGuid(const wowee::network::Packet& packet) { + if (packet.getReadPos() >= packet.getSize()) { + return false; + } + + const auto& rawData = packet.getData(); + const uint8_t mask = rawData[packet.getReadPos()]; + size_t guidBytes = 1; + for (int bit = 0; bit < 8; ++bit) { + if ((mask & (1u << bit)) != 0) { + ++guidBytes; + } + } + return packet.getSize() - packet.getReadPos() >= guidBytes; + } } namespace wowee { @@ -3684,7 +3700,7 @@ bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) { // Read target flags and target (simplified) if (packet.getSize() - packet.getReadPos() >= 4) { uint32_t targetFlags = packet.readUInt32(); - if ((targetFlags & 0x02) && packet.getSize() - packet.getReadPos() >= 1) { // TARGET_FLAG_UNIT, validate packed GUID read + if ((targetFlags & 0x02) && hasFullPackedGuid(packet)) { // TARGET_FLAG_UNIT data.targetGuid = UpdateObjectParser::readPackedGuid(packet); } } @@ -3723,7 +3739,7 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { data.hitTargets.reserve(storedHitLimit); for (uint16_t i = 0; i < rawHitCount; ++i) { // WotLK hit targets are packed GUIDs, like the caster and miss targets. - if (packet.getSize() - packet.getReadPos() < 1) { + if (!hasFullPackedGuid(packet)) { LOG_WARNING("Spell go: truncated hit targets at index ", i, "/", (int)rawHitCount); break; } @@ -3749,13 +3765,17 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { for (uint16_t i = 0; i < rawMissCount; ++i) { // Each miss entry: packed GUID(1-8 bytes) + missType(1 byte). // REFLECT additionally appends uint32 reflectSpellId + uint8 reflectResult. - if (packet.getSize() - packet.getReadPos() < 2) { + if (!hasFullPackedGuid(packet)) { LOG_WARNING("Spell go: truncated miss targets at index ", i, "/", (int)rawMissCount); break; } SpellGoMissEntry m; m.targetGuid = UpdateObjectParser::readPackedGuid(packet); // packed GUID in WotLK - m.missType = (packet.getSize() - packet.getReadPos() >= 1) ? packet.readUInt8() : 0; + if (packet.getSize() - packet.getReadPos() < 1) { + LOG_WARNING("Spell go: missing missType at miss index ", i, "/", (int)rawMissCount); + break; + } + m.missType = packet.readUInt8(); if (m.missType == 11) { if (packet.getSize() - packet.getReadPos() < 5) { LOG_WARNING("Spell go: truncated reflect payload at miss index ", i, "/", (int)rawMissCount); From f2204f9d7be430b0d2443187c1d55b5eb26155fb Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 10:25:07 -0700 Subject: [PATCH 08/31] fix(combatlog): validate packed guid bounds in classic spell cast parsers --- src/game/packet_parsers_classic.cpp | 30 ++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index 4528ef72..3d3e1a92 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -358,8 +358,9 @@ bool ClassicPacketParsers::parseSpellStart(network::Packet& packet, SpellStartDa auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; if (rem() < 2) return false; + if (!hasFullPackedGuid(packet)) return false; data.casterGuid = UpdateObjectParser::readPackedGuid(packet); - if (rem() < 1) return false; + if (!hasFullPackedGuid(packet)) return false; data.casterUnit = UpdateObjectParser::readPackedGuid(packet); // uint8 castCount + uint32 spellId + uint16 castFlags + uint32 castTime = 11 bytes @@ -373,7 +374,7 @@ bool ClassicPacketParsers::parseSpellStart(network::Packet& packet, SpellStartDa if (rem() < 2) return true; uint16_t targetFlags = packet.readUInt16(); // TARGET_FLAG_UNIT (0x02) or TARGET_FLAG_OBJECT (0x800) carry a packed GUID - if (((targetFlags & 0x02) || (targetFlags & 0x800)) && rem() >= 1) { + if (((targetFlags & 0x02) || (targetFlags & 0x800)) && hasFullPackedGuid(packet)) { data.targetGuid = UpdateObjectParser::readPackedGuid(packet); } @@ -398,8 +399,9 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; if (rem() < 2) return false; + if (!hasFullPackedGuid(packet)) return false; data.casterGuid = UpdateObjectParser::readPackedGuid(packet); - if (rem() < 1) return false; + if (!hasFullPackedGuid(packet)) return false; data.casterUnit = UpdateObjectParser::readPackedGuid(packet); // uint8 castCount + uint32 spellId + uint16 castFlags = 7 bytes @@ -416,16 +418,21 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da } const uint8_t storedHitLimit = std::min(rawHitCount, 128); data.hitTargets.reserve(storedHitLimit); - for (uint16_t i = 0; i < rawHitCount && rem() >= 1; ++i) { + uint16_t parsedHitCount = 0; + for (uint16_t i = 0; i < rawHitCount; ++i) { + if (!hasFullPackedGuid(packet)) { + break; + } const uint64_t targetGuid = UpdateObjectParser::readPackedGuid(packet); + ++parsedHitCount; if (i < storedHitLimit) { data.hitTargets.push_back(targetGuid); } } data.hitCount = static_cast(data.hitTargets.size()); // Check if we read all expected hits - if (data.hitTargets.size() < rawHitCount) { - LOG_WARNING("[Classic] Spell go: truncated hit targets at index ", (int)data.hitTargets.size(), + if (parsedHitCount < rawHitCount) { + LOG_WARNING("[Classic] Spell go: truncated hit targets at index ", (int)parsedHitCount, "/", (int)rawHitCount); } @@ -437,7 +444,11 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da } const uint8_t storedMissLimit = std::min(rawMissCount, 128); data.missTargets.reserve(storedMissLimit); - for (uint16_t i = 0; i < rawMissCount && rem() >= 2; ++i) { + uint16_t parsedMissCount = 0; + for (uint16_t i = 0; i < rawMissCount; ++i) { + if (!hasFullPackedGuid(packet)) { + break; + } SpellGoMissEntry m; m.targetGuid = UpdateObjectParser::readPackedGuid(packet); if (rem() < 1) break; @@ -447,14 +458,15 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da (void)packet.readUInt32(); (void)packet.readUInt8(); } + ++parsedMissCount; if (i < storedMissLimit) { data.missTargets.push_back(m); } } data.missCount = static_cast(data.missTargets.size()); // Check if we read all expected misses - if (data.missTargets.size() < rawMissCount) { - LOG_WARNING("[Classic] Spell go: truncated miss targets at index ", (int)data.missTargets.size(), + if (parsedMissCount < rawMissCount) { + LOG_WARNING("[Classic] Spell go: truncated miss targets at index ", (int)parsedMissCount, "/", (int)rawMissCount); } From c32dbb082d69feaa27361384dd5d11e402986594 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 10:33:48 -0700 Subject: [PATCH 09/31] fix(combatlog): validate packed GUID bounds in spell start parser --- src/game/world_packets.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 02c20a6c..cc04e449 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -3683,7 +3683,14 @@ bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) { if (packet.getSize() - packet.getReadPos() < 22) return false; size_t startPos = packet.getReadPos(); + if (!hasFullPackedGuid(packet)) { + return false; + } data.casterGuid = UpdateObjectParser::readPackedGuid(packet); + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; + } data.casterUnit = UpdateObjectParser::readPackedGuid(packet); // Validate remaining fixed fields (castCount + spellId + castFlags + castTime = 9 bytes) From 98267d6517ae4f4c8d5d81dc8cd031ef21864c43 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 10:40:54 -0700 Subject: [PATCH 10/31] fix(combatlog): validate packed guid bounds in spell go parser --- src/game/world_packets.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index cc04e449..c52be440 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -3722,7 +3722,14 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { if (packet.getSize() - packet.getReadPos() < 17) return false; size_t startPos = packet.getReadPos(); + if (!hasFullPackedGuid(packet)) { + return false; + } data.casterGuid = UpdateObjectParser::readPackedGuid(packet); + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; + } data.casterUnit = UpdateObjectParser::readPackedGuid(packet); // Validate remaining fixed fields up to hitCount/missCount From 43cc2635acd61292607d89e2371cc7e10b82f7ca Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 10:48:20 -0700 Subject: [PATCH 11/31] fix(combatlog): validate packed GUID bounds in attacker state parsers --- src/game/packet_parsers_classic.cpp | 10 +++++++++- src/game/world_packets.cpp | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index 3d3e1a92..e99329b3 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -489,9 +489,17 @@ bool ClassicPacketParsers::parseAttackerStateUpdate(network::Packet& packet, Att auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; if (rem() < 5) return false; // hitInfo(4) + at least GUID mask byte(1) + const size_t startPos = packet.getReadPos(); data.hitInfo = packet.readUInt32(); + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; + } data.attackerGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla - if (rem() < 1) return false; + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; + } data.targetGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla if (rem() < 5) return false; // int32 totalDamage + uint8 subDamageCount diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index c52be440..82126379 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -3343,7 +3343,15 @@ bool AttackerStateUpdateParser::parse(network::Packet& packet, AttackerStateUpda size_t startPos = packet.getReadPos(); data.hitInfo = packet.readUInt32(); + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; + } data.attackerGuid = UpdateObjectParser::readPackedGuid(packet); + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; + } data.targetGuid = UpdateObjectParser::readPackedGuid(packet); // Validate totalDamage + subDamageCount can be read (5 bytes) From 0c7dfdebe94cc3b0e69d58c05a018967ce74c269 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 10:56:04 -0700 Subject: [PATCH 12/31] fix(combatlog): fail spell go parse on truncated target lists --- src/game/world_packets.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 82126379..c176650e 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -3758,11 +3758,14 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { } const uint8_t storedHitLimit = std::min(rawHitCount, 128); + bool truncatedTargets = false; + data.hitTargets.reserve(storedHitLimit); for (uint16_t i = 0; i < rawHitCount; ++i) { // WotLK hit targets are packed GUIDs, like the caster and miss targets. if (!hasFullPackedGuid(packet)) { LOG_WARNING("Spell go: truncated hit targets at index ", i, "/", (int)rawHitCount); + truncatedTargets = true; break; } const uint64_t targetGuid = UpdateObjectParser::readPackedGuid(packet); @@ -3770,6 +3773,10 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { data.hitTargets.push_back(targetGuid); } } + if (truncatedTargets) { + packet.setReadPos(startPos); + return false; + } data.hitCount = static_cast(data.hitTargets.size()); // Validate missCount field exists @@ -3789,18 +3796,21 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { // REFLECT additionally appends uint32 reflectSpellId + uint8 reflectResult. if (!hasFullPackedGuid(packet)) { LOG_WARNING("Spell go: truncated miss targets at index ", i, "/", (int)rawMissCount); + truncatedTargets = true; break; } SpellGoMissEntry m; m.targetGuid = UpdateObjectParser::readPackedGuid(packet); // packed GUID in WotLK if (packet.getSize() - packet.getReadPos() < 1) { LOG_WARNING("Spell go: missing missType at miss index ", i, "/", (int)rawMissCount); + truncatedTargets = true; break; } m.missType = packet.readUInt8(); if (m.missType == 11) { if (packet.getSize() - packet.getReadPos() < 5) { LOG_WARNING("Spell go: truncated reflect payload at miss index ", i, "/", (int)rawMissCount); + truncatedTargets = true; break; } (void)packet.readUInt32(); @@ -3810,6 +3820,10 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { data.missTargets.push_back(m); } } + if (truncatedTargets) { + packet.setReadPos(startPos); + return false; + } data.missCount = static_cast(data.missTargets.size()); LOG_DEBUG("Spell go: spell=", data.spellId, " hits=", (int)data.hitCount, From 2006b71d4837e474aaee28f57cb68ff0216f1726 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 11:03:02 -0700 Subject: [PATCH 13/31] fix(combatlog): enforce full spell start fixed-field bounds --- src/game/world_packets.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index c176650e..25dc79d2 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -3701,8 +3701,8 @@ bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) { } data.casterUnit = UpdateObjectParser::readPackedGuid(packet); - // Validate remaining fixed fields (castCount + spellId + castFlags + castTime = 9 bytes) - if (packet.getSize() - packet.getReadPos() < 9) { + // Validate remaining fixed fields (castCount + spellId + castFlags + castTime = 13 bytes) + if (packet.getSize() - packet.getReadPos() < 13) { packet.setReadPos(startPos); return false; } From debc47b5ccaab350bf116b129e001f4b65436efb Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 11:10:08 -0700 Subject: [PATCH 14/31] fix(combatlog): fail classic and tbc spell go parse on truncation --- src/game/packet_parsers_classic.cpp | 44 ++++++++++++++++++----------- src/game/packet_parsers_tbc.cpp | 39 +++++++++++++++++-------- 2 files changed, 55 insertions(+), 28 deletions(-) diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index e99329b3..995f109b 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -397,6 +397,7 @@ bool ClassicPacketParsers::parseSpellStart(network::Packet& packet, SpellStartDa // ============================================================================ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) { auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; + const size_t startPos = packet.getReadPos(); if (rem() < 2) return false; if (!hasFullPackedGuid(packet)) return false; @@ -418,23 +419,24 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da } const uint8_t storedHitLimit = std::min(rawHitCount, 128); data.hitTargets.reserve(storedHitLimit); - uint16_t parsedHitCount = 0; + bool truncatedTargets = false; for (uint16_t i = 0; i < rawHitCount; ++i) { if (!hasFullPackedGuid(packet)) { + LOG_WARNING("[Classic] Spell go: truncated hit targets at index ", i, + "/", (int)rawHitCount); + truncatedTargets = true; break; } const uint64_t targetGuid = UpdateObjectParser::readPackedGuid(packet); - ++parsedHitCount; if (i < storedHitLimit) { data.hitTargets.push_back(targetGuid); } } - data.hitCount = static_cast(data.hitTargets.size()); - // Check if we read all expected hits - if (parsedHitCount < rawHitCount) { - LOG_WARNING("[Classic] Spell go: truncated hit targets at index ", (int)parsedHitCount, - "/", (int)rawHitCount); + if (truncatedTargets) { + packet.setReadPos(startPos); + return false; } + data.hitCount = static_cast(data.hitTargets.size()); // Miss targets if (rem() < 1) return true; @@ -444,31 +446,41 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da } const uint8_t storedMissLimit = std::min(rawMissCount, 128); data.missTargets.reserve(storedMissLimit); - uint16_t parsedMissCount = 0; for (uint16_t i = 0; i < rawMissCount; ++i) { if (!hasFullPackedGuid(packet)) { + LOG_WARNING("[Classic] Spell go: truncated miss targets at index ", i, + "/", (int)rawMissCount); + truncatedTargets = true; break; } SpellGoMissEntry m; m.targetGuid = UpdateObjectParser::readPackedGuid(packet); - if (rem() < 1) break; + if (rem() < 1) { + LOG_WARNING("[Classic] Spell go: missing missType at miss index ", i, + "/", (int)rawMissCount); + truncatedTargets = true; + break; + } m.missType = packet.readUInt8(); if (m.missType == 11) { - if (rem() < 5) break; + if (rem() < 5) { + LOG_WARNING("[Classic] Spell go: truncated reflect payload at miss index ", i, + "/", (int)rawMissCount); + truncatedTargets = true; + break; + } (void)packet.readUInt32(); (void)packet.readUInt8(); } - ++parsedMissCount; if (i < storedMissLimit) { data.missTargets.push_back(m); } } - data.missCount = static_cast(data.missTargets.size()); - // Check if we read all expected misses - if (parsedMissCount < rawMissCount) { - LOG_WARNING("[Classic] Spell go: truncated miss targets at index ", (int)parsedMissCount, - "/", (int)rawMissCount); + if (truncatedTargets) { + packet.setReadPos(startPos); + return false; } + data.missCount = static_cast(data.missTargets.size()); LOG_DEBUG("[Classic] Spell go: spell=", data.spellId, " hits=", (int)data.hitCount, " misses=", (int)data.missCount); diff --git a/src/game/packet_parsers_tbc.cpp b/src/game/packet_parsers_tbc.cpp index ba2393c3..34f5b283 100644 --- a/src/game/packet_parsers_tbc.cpp +++ b/src/game/packet_parsers_tbc.cpp @@ -1261,6 +1261,7 @@ bool TbcPacketParsers::parseSpellStart(network::Packet& packet, SpellStartData& // WotLK uses packed GUIDs and adds a timestamp (u32) after castFlags. // ============================================================================ bool TbcPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) { + const size_t startPos = packet.getReadPos(); // Fixed header before hit/miss lists: // casterGuid(u64) + casterUnit(u64) + castCount(u8) + spellId(u32) + castFlags(u32) if (packet.getSize() - packet.getReadPos() < 25) return false; @@ -1283,18 +1284,24 @@ bool TbcPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) } const uint8_t storedHitLimit = std::min(rawHitCount, 128); data.hitTargets.reserve(storedHitLimit); - for (uint16_t i = 0; i < rawHitCount && packet.getReadPos() + 8 <= packet.getSize(); ++i) { + bool truncatedTargets = false; + for (uint16_t i = 0; i < rawHitCount; ++i) { + if (packet.getReadPos() + 8 > packet.getSize()) { + LOG_WARNING("[TBC] Spell go: truncated hit targets at index ", i, + "/", (int)rawHitCount); + truncatedTargets = true; + break; + } const uint64_t targetGuid = packet.readUInt64(); // full GUID in TBC if (i < storedHitLimit) { data.hitTargets.push_back(targetGuid); } } - data.hitCount = static_cast(data.hitTargets.size()); - // Check if we read all expected hits - if (data.hitTargets.size() < rawHitCount) { - LOG_WARNING("[TBC] Spell go: truncated hit targets at index ", (int)data.hitTargets.size(), - "/", (int)rawHitCount); + if (truncatedTargets) { + packet.setReadPos(startPos); + return false; } + data.hitCount = static_cast(data.hitTargets.size()); if (packet.getReadPos() < packet.getSize()) { const uint8_t rawMissCount = packet.readUInt8(); @@ -1303,12 +1310,21 @@ bool TbcPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) } const uint8_t storedMissLimit = std::min(rawMissCount, 128); data.missTargets.reserve(storedMissLimit); - for (uint16_t i = 0; i < rawMissCount && packet.getReadPos() + 9 <= packet.getSize(); ++i) { + for (uint16_t i = 0; i < rawMissCount; ++i) { + if (packet.getReadPos() + 9 > packet.getSize()) { + LOG_WARNING("[TBC] Spell go: truncated miss targets at index ", i, + "/", (int)rawMissCount); + truncatedTargets = true; + break; + } SpellGoMissEntry m; m.targetGuid = packet.readUInt64(); // full GUID in TBC m.missType = packet.readUInt8(); if (m.missType == 11) { if (packet.getReadPos() + 5 > packet.getSize()) { + LOG_WARNING("[TBC] Spell go: truncated reflect payload at miss index ", i, + "/", (int)rawMissCount); + truncatedTargets = true; break; } (void)packet.readUInt32(); @@ -1318,12 +1334,11 @@ bool TbcPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) data.missTargets.push_back(m); } } - data.missCount = static_cast(data.missTargets.size()); - // Check if we read all expected misses - if (data.missTargets.size() < rawMissCount) { - LOG_WARNING("[TBC] Spell go: truncated miss targets at index ", (int)data.missTargets.size(), - "/", (int)rawMissCount); + if (truncatedTargets) { + packet.setReadPos(startPos); + return false; } + data.missCount = static_cast(data.missTargets.size()); } LOG_DEBUG("[TBC] Spell go: spell=", data.spellId, " hits=", (int)data.hitCount, From 3dba13bbd262cd8c22468a9f802febc776cae4e4 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 13:21:38 -0700 Subject: [PATCH 15/31] fix(combatlog): reset spell go parser output before decode --- src/game/packet_parsers_classic.cpp | 3 +++ src/game/packet_parsers_tbc.cpp | 3 +++ src/game/world_packets.cpp | 3 +++ 3 files changed, 9 insertions(+) diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index 995f109b..54fb7f95 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -396,6 +396,9 @@ bool ClassicPacketParsers::parseSpellStart(network::Packet& packet, SpellStartDa // + uint8(missCount) + [PackedGuid(missTarget) + uint8(missType)] × missCount // ============================================================================ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) { + // Always reset output to avoid stale targets when callers reuse buffers. + data = SpellGoData{}; + auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; const size_t startPos = packet.getReadPos(); if (rem() < 2) return false; diff --git a/src/game/packet_parsers_tbc.cpp b/src/game/packet_parsers_tbc.cpp index 34f5b283..16f1ffed 100644 --- a/src/game/packet_parsers_tbc.cpp +++ b/src/game/packet_parsers_tbc.cpp @@ -1261,6 +1261,9 @@ bool TbcPacketParsers::parseSpellStart(network::Packet& packet, SpellStartData& // WotLK uses packed GUIDs and adds a timestamp (u32) after castFlags. // ============================================================================ bool TbcPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) { + // Always reset output to avoid stale targets when callers reuse buffers. + data = SpellGoData{}; + const size_t startPos = packet.getReadPos(); // Fixed header before hit/miss lists: // casterGuid(u64) + casterUnit(u64) + castCount(u8) + spellId(u32) + castFlags(u32) diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 25dc79d2..f057f90b 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -3725,6 +3725,9 @@ bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) { } bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { + // Always reset output to avoid stale targets when callers reuse buffers. + data = SpellGoData{}; + // Packed GUIDs are variable-length, so only require the smallest possible // shape up front: 2 GUID masks + fixed fields through missCount. if (packet.getSize() - packet.getReadPos() < 17) return false; From 4e97a19b23444a929090e0e78c875aedf679ede6 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 13:29:55 -0700 Subject: [PATCH 16/31] fix(combatlog): avoid partial spell miss log entries on truncation --- src/game/game_handler.cpp | 44 ++++++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index fec1caaa..c7c69c26 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2760,25 +2760,55 @@ void GameHandler::handlePacket(network::Packet& packet) { uint64_t casterGuid = readSpellMissGuid(); if (packet.getSize() - packet.getReadPos() < 5) break; /*uint8_t unk =*/ packet.readUInt8(); - uint32_t count = packet.readUInt32(); - for (uint32_t i = 0; i < count; ++i) { + const uint32_t rawCount = packet.readUInt32(); + if (rawCount > 128) { + LOG_WARNING("SMSG_SPELLLOGMISS: miss count capped (requested=", rawCount, ")"); + } + const uint32_t storedLimit = std::min(rawCount, 128u); + + struct SpellMissLogEntry { + uint64_t victimGuid = 0; + uint8_t missInfo = 0; + }; + std::vector parsedMisses; + parsedMisses.reserve(storedLimit); + + bool truncated = false; + for (uint32_t i = 0; i < rawCount; ++i) { if (packet.getSize() - packet.getReadPos() < (spellMissUsesFullGuid ? 9u : 2u) || (!spellMissUsesFullGuid && !hasFullPackedGuid(packet))) { - packet.setReadPos(packet.getSize()); break; + truncated = true; + break; } - uint64_t victimGuid = readSpellMissGuid(); - if (packet.getSize() - packet.getReadPos() < 1) break; - uint8_t missInfo = packet.readUInt8(); + const uint64_t victimGuid = readSpellMissGuid(); + if (packet.getSize() - packet.getReadPos() < 1) { + truncated = true; + break; + } + const uint8_t missInfo = packet.readUInt8(); // REFLECT (11): extra uint32 reflectSpellId + uint8 reflectResult if (missInfo == 11) { if (packet.getSize() - packet.getReadPos() >= 5) { /*uint32_t reflectSpellId =*/ packet.readUInt32(); /*uint8_t reflectResult =*/ packet.readUInt8(); } else { - packet.setReadPos(packet.getSize()); + truncated = true; break; } } + if (i < storedLimit) { + parsedMisses.push_back({victimGuid, missInfo}); + } + } + + if (truncated) { + packet.setReadPos(packet.getSize()); + break; + } + + for (const auto& miss : parsedMisses) { + const uint64_t victimGuid = miss.victimGuid; + const uint8_t missInfo = miss.missInfo; CombatTextEntry::Type ct = combatTextTypeFromSpellMissInfo(missInfo); if (casterGuid == playerGuid) { // We cast a spell and it missed the target From fbcbdc29357457adf33a5058f8aad45ea1518af2 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 13:37:28 -0700 Subject: [PATCH 17/31] fix(combatlog): reject truncated spell go packets missing counts --- src/game/packet_parsers_classic.cpp | 16 +++++-- src/game/packet_parsers_tbc.cpp | 67 ++++++++++++++++------------- src/game/world_packets.cpp | 6 ++- 3 files changed, 52 insertions(+), 37 deletions(-) diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index 54fb7f95..1e801b6b 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -414,8 +414,12 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da data.spellId = packet.readUInt32(); data.castFlags = packet.readUInt16(); // uint16 in Vanilla (uint32 in TBC/WotLK) - // Hit targets - if (rem() < 1) return true; + // hitCount is mandatory in SMSG_SPELL_GO. Missing byte means truncation. + if (rem() < 1) { + LOG_WARNING("[Classic] Spell go: missing hitCount after fixed fields"); + packet.setReadPos(startPos); + return false; + } const uint8_t rawHitCount = packet.readUInt8(); if (rawHitCount > 128) { LOG_WARNING("[Classic] Spell go: hitCount capped (requested=", (int)rawHitCount, ")"); @@ -441,8 +445,12 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da } data.hitCount = static_cast(data.hitTargets.size()); - // Miss targets - if (rem() < 1) return true; + // missCount is mandatory in SMSG_SPELL_GO. Missing byte means truncation. + if (rem() < 1) { + LOG_WARNING("[Classic] Spell go: missing missCount after hit target list"); + packet.setReadPos(startPos); + return false; + } const uint8_t rawMissCount = packet.readUInt8(); if (rawMissCount > 128) { LOG_WARNING("[Classic] Spell go: missCount capped (requested=", (int)rawMissCount, ")"); diff --git a/src/game/packet_parsers_tbc.cpp b/src/game/packet_parsers_tbc.cpp index 16f1ffed..ca36930a 100644 --- a/src/game/packet_parsers_tbc.cpp +++ b/src/game/packet_parsers_tbc.cpp @@ -1277,8 +1277,9 @@ bool TbcPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) // NOTE: NO timestamp field here in TBC (WotLK added packet.readUInt32()) if (packet.getReadPos() >= packet.getSize()) { - LOG_DEBUG("[TBC] Spell go: spell=", data.spellId, " (no hit data)"); - return true; + LOG_WARNING("[TBC] Spell go: missing hitCount after fixed fields"); + packet.setReadPos(startPos); + return false; } const uint8_t rawHitCount = packet.readUInt8(); @@ -1306,43 +1307,47 @@ bool TbcPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) } data.hitCount = static_cast(data.hitTargets.size()); - if (packet.getReadPos() < packet.getSize()) { - const uint8_t rawMissCount = packet.readUInt8(); - if (rawMissCount > 128) { - LOG_WARNING("[TBC] Spell go: missCount capped (requested=", (int)rawMissCount, ")"); + if (packet.getReadPos() >= packet.getSize()) { + LOG_WARNING("[TBC] Spell go: missing missCount after hit target list"); + packet.setReadPos(startPos); + return false; + } + + const uint8_t rawMissCount = packet.readUInt8(); + if (rawMissCount > 128) { + LOG_WARNING("[TBC] Spell go: missCount capped (requested=", (int)rawMissCount, ")"); + } + const uint8_t storedMissLimit = std::min(rawMissCount, 128); + data.missTargets.reserve(storedMissLimit); + for (uint16_t i = 0; i < rawMissCount; ++i) { + if (packet.getReadPos() + 9 > packet.getSize()) { + LOG_WARNING("[TBC] Spell go: truncated miss targets at index ", i, + "/", (int)rawMissCount); + truncatedTargets = true; + break; } - const uint8_t storedMissLimit = std::min(rawMissCount, 128); - data.missTargets.reserve(storedMissLimit); - for (uint16_t i = 0; i < rawMissCount; ++i) { - if (packet.getReadPos() + 9 > packet.getSize()) { - LOG_WARNING("[TBC] Spell go: truncated miss targets at index ", i, + SpellGoMissEntry m; + m.targetGuid = packet.readUInt64(); // full GUID in TBC + m.missType = packet.readUInt8(); + if (m.missType == 11) { + if (packet.getReadPos() + 5 > packet.getSize()) { + LOG_WARNING("[TBC] Spell go: truncated reflect payload at miss index ", i, "/", (int)rawMissCount); truncatedTargets = true; break; } - SpellGoMissEntry m; - m.targetGuid = packet.readUInt64(); // full GUID in TBC - m.missType = packet.readUInt8(); - if (m.missType == 11) { - if (packet.getReadPos() + 5 > packet.getSize()) { - LOG_WARNING("[TBC] Spell go: truncated reflect payload at miss index ", i, - "/", (int)rawMissCount); - truncatedTargets = true; - break; - } - (void)packet.readUInt32(); - (void)packet.readUInt8(); - } - if (i < storedMissLimit) { - data.missTargets.push_back(m); - } + (void)packet.readUInt32(); + (void)packet.readUInt8(); } - if (truncatedTargets) { - packet.setReadPos(startPos); - return false; + if (i < storedMissLimit) { + data.missTargets.push_back(m); } - data.missCount = static_cast(data.missTargets.size()); } + if (truncatedTargets) { + packet.setReadPos(startPos); + return false; + } + data.missCount = static_cast(data.missTargets.size()); LOG_DEBUG("[TBC] Spell go: spell=", data.spellId, " hits=", (int)data.hitCount, " misses=", (int)data.missCount); diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index f057f90b..5a23e1c9 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -3782,9 +3782,11 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { } data.hitCount = static_cast(data.hitTargets.size()); - // Validate missCount field exists + // missCount is mandatory in SMSG_SPELL_GO. Missing byte means truncation. if (packet.getSize() - packet.getReadPos() < 1) { - return true; // Valid, just no misses + LOG_WARNING("Spell go: missing missCount after hit target list"); + packet.setReadPos(startPos); + return false; } const uint8_t rawMissCount = packet.readUInt8(); From 96fc315c47f994ca046af75e1d2796b87bd92ba8 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 13:44:37 -0700 Subject: [PATCH 18/31] fix(combatlog): reject truncated spell start target GUIDs --- src/game/packet_parsers_classic.cpp | 3 ++- src/game/packet_parsers_tbc.cpp | 6 +++++- src/game/world_packets.cpp | 7 ++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index 1e801b6b..2dde86c7 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -374,7 +374,8 @@ bool ClassicPacketParsers::parseSpellStart(network::Packet& packet, SpellStartDa if (rem() < 2) return true; uint16_t targetFlags = packet.readUInt16(); // TARGET_FLAG_UNIT (0x02) or TARGET_FLAG_OBJECT (0x800) carry a packed GUID - if (((targetFlags & 0x02) || (targetFlags & 0x800)) && hasFullPackedGuid(packet)) { + if ((targetFlags & 0x02) || (targetFlags & 0x800)) { + if (!hasFullPackedGuid(packet)) return false; data.targetGuid = UpdateObjectParser::readPackedGuid(packet); } diff --git a/src/game/packet_parsers_tbc.cpp b/src/game/packet_parsers_tbc.cpp index ca36930a..f12e86e5 100644 --- a/src/game/packet_parsers_tbc.cpp +++ b/src/game/packet_parsers_tbc.cpp @@ -1245,7 +1245,11 @@ bool TbcPacketParsers::parseSpellStart(network::Packet& packet, SpellStartData& if (packet.getReadPos() + 4 <= packet.getSize()) { uint32_t targetFlags = packet.readUInt32(); - if ((targetFlags & 0x02) && packet.getReadPos() + 8 <= packet.getSize()) { + const bool needsTargetGuid = (targetFlags & 0x02) || (targetFlags & 0x800); // UNIT/OBJECT + if (needsTargetGuid) { + if (packet.getReadPos() + 8 > packet.getSize()) { + return false; + } data.targetGuid = packet.readUInt64(); // full GUID in TBC } } diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 5a23e1c9..616e9633 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -3715,7 +3715,12 @@ bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) { // Read target flags and target (simplified) if (packet.getSize() - packet.getReadPos() >= 4) { uint32_t targetFlags = packet.readUInt32(); - if ((targetFlags & 0x02) && hasFullPackedGuid(packet)) { // TARGET_FLAG_UNIT + const bool needsTargetGuid = (targetFlags & 0x02) || (targetFlags & 0x800); // UNIT/OBJECT + if (needsTargetGuid) { + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; + } data.targetGuid = UpdateObjectParser::readPackedGuid(packet); } } From 51ef28e3f72f51e6f71d9eb367d4e3e33500c586 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 13:51:37 -0700 Subject: [PATCH 19/31] fix(combatlog): validate packed GUID bounds in spell damage/heal logs --- src/game/world_packets.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 616e9633..71252846 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -3427,7 +3427,15 @@ bool SpellDamageLogParser::parse(network::Packet& packet, SpellDamageLogData& da if (packet.getSize() - packet.getReadPos() < 30) return false; size_t startPos = packet.getReadPos(); + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; + } data.targetGuid = UpdateObjectParser::readPackedGuid(packet); + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; + } data.attackerGuid = UpdateObjectParser::readPackedGuid(packet); // Validate core fields (spellId + damage + overkill + schoolMask + absorbed + resisted = 21 bytes) @@ -3469,7 +3477,15 @@ bool SpellHealLogParser::parse(network::Packet& packet, SpellHealLogData& data) if (packet.getSize() - packet.getReadPos() < 21) return false; size_t startPos = packet.getReadPos(); + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; + } data.targetGuid = UpdateObjectParser::readPackedGuid(packet); + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; + } data.casterGuid = UpdateObjectParser::readPackedGuid(packet); // Validate remaining fields (spellId + heal + overheal + absorbed + critFlag = 17 bytes) From b8cf86781469d3a931cde9f2ae930a3d3832f848 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 13:59:07 -0700 Subject: [PATCH 20/31] fix(combatlog): enforce TBC spell damage/heal packet bounds --- src/game/packet_parsers_tbc.cpp | 46 ++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/src/game/packet_parsers_tbc.cpp b/src/game/packet_parsers_tbc.cpp index f12e86e5..b988eb66 100644 --- a/src/game/packet_parsers_tbc.cpp +++ b/src/game/packet_parsers_tbc.cpp @@ -1436,26 +1436,38 @@ bool TbcPacketParsers::parseAttackerStateUpdate(network::Packet& packet, Attacke // TBC uses full uint64 GUIDs; WotLK uses packed GUIDs. // ============================================================================ bool TbcPacketParsers::parseSpellDamageLog(network::Packet& packet, SpellDamageLogData& data) { - if (packet.getSize() - packet.getReadPos() < 29) return false; + // Fixed TBC payload size: + // targetGuid(8) + attackerGuid(8) + spellId(4) + damage(4) + schoolMask(1) + // + absorbed(4) + resisted(4) + periodicLog(1) + unused(1) + blocked(4) + flags(4) + // = 43 bytes + if (packet.getSize() - packet.getReadPos() < 43) return false; - data.targetGuid = packet.readUInt64(); // full GUID in TBC + data = SpellDamageLogData{}; + + const size_t startPos = packet.getReadPos(); + data.targetGuid = packet.readUInt64(); // full GUID in TBC data.attackerGuid = packet.readUInt64(); // full GUID in TBC - data.spellId = packet.readUInt32(); - data.damage = packet.readUInt32(); - data.schoolMask = packet.readUInt8(); - data.absorbed = packet.readUInt32(); - data.resisted = packet.readUInt32(); + data.spellId = packet.readUInt32(); + data.damage = packet.readUInt32(); + data.schoolMask = packet.readUInt8(); + data.absorbed = packet.readUInt32(); + data.resisted = packet.readUInt32(); uint8_t periodicLog = packet.readUInt8(); (void)periodicLog; - packet.readUInt8(); // unused - packet.readUInt32(); // blocked + packet.readUInt8(); // unused + packet.readUInt32(); // blocked uint32_t flags = packet.readUInt32(); data.isCrit = (flags & 0x02) != 0; // TBC does not have an overkill field here data.overkill = 0; + if (packet.getReadPos() - startPos != 43) { + packet.setReadPos(startPos); + return false; + } + LOG_DEBUG("[TBC] Spell damage: spellId=", data.spellId, " dmg=", data.damage, data.isCrit ? " CRIT" : ""); return true; @@ -1467,13 +1479,17 @@ bool TbcPacketParsers::parseSpellDamageLog(network::Packet& packet, SpellDamageL // TBC uses full uint64 GUIDs; WotLK uses packed GUIDs. // ============================================================================ bool TbcPacketParsers::parseSpellHealLog(network::Packet& packet, SpellHealLogData& data) { - if (packet.getSize() - packet.getReadPos() < 25) return false; + // Fixed payload is 28 bytes; many cores append crit flag (1 byte). + // targetGuid(8) + casterGuid(8) + spellId(4) + heal(4) + overheal(4) + if (packet.getSize() - packet.getReadPos() < 28) return false; - data.targetGuid = packet.readUInt64(); // full GUID in TBC - data.casterGuid = packet.readUInt64(); // full GUID in TBC - data.spellId = packet.readUInt32(); - data.heal = packet.readUInt32(); - data.overheal = packet.readUInt32(); + data = SpellHealLogData{}; + + data.targetGuid = packet.readUInt64(); // full GUID in TBC + data.casterGuid = packet.readUInt64(); // full GUID in TBC + data.spellId = packet.readUInt32(); + data.heal = packet.readUInt32(); + data.overheal = packet.readUInt32(); // TBC has no absorbed field in SMSG_SPELLHEALLOG; skip crit flag if (packet.getReadPos() < packet.getSize()) { uint8_t critFlag = packet.readUInt8(); From 4f5c05119974a1bb4e90377fd5822bc3fc517fdb Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 14:06:19 -0700 Subject: [PATCH 21/31] fix(combatlog): relax packed GUID minimum-size gates --- src/game/world_packets.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 71252846..4656f086 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -3703,8 +3703,9 @@ bool CastFailedParser::parse(network::Packet& packet, CastFailedData& data) { } bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) { - // Upfront validation: packed GUID(1-8) + packed GUID(1-8) + castCount(1) + spellId(4) + castFlags(4) + castTime(4) = 22 bytes minimum - if (packet.getSize() - packet.getReadPos() < 22) return false; + // Packed GUIDs are variable-length; only require minimal packet shape up front: + // two GUID masks + castCount(1) + spellId(4) + castFlags(4) + castTime(4). + if (packet.getSize() - packet.getReadPos() < 15) return false; size_t startPos = packet.getReadPos(); if (!hasFullPackedGuid(packet)) { @@ -3750,8 +3751,8 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { data = SpellGoData{}; // Packed GUIDs are variable-length, so only require the smallest possible - // shape up front: 2 GUID masks + fixed fields through missCount. - if (packet.getSize() - packet.getReadPos() < 17) return false; + // shape up front: 2 GUID masks + fixed fields through hitCount. + if (packet.getSize() - packet.getReadPos() < 16) return false; size_t startPos = packet.getReadPos(); if (!hasFullPackedGuid(packet)) { From 87359f0e1c5fb688d3841be717f38bacd1b06e68 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 14:13:39 -0700 Subject: [PATCH 22/31] fix(combatlog): validate packed GUID bounds in spell energize log --- src/game/game_handler.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index c7c69c26..f629002c 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -4126,11 +4126,13 @@ void GameHandler::handlePacket(network::Packet& packet) { return (packet.getSize() - packet.getReadPos() >= 8) ? packet.readUInt64() : 0; return UpdateObjectParser::readPackedGuid(packet); }; - if (packet.getSize() - packet.getReadPos() < (energizeTbc ? 8u : 1u)) { + if (packet.getSize() - packet.getReadPos() < (energizeTbc ? 8u : 1u) + || (!energizeTbc && !hasFullPackedGuid(packet))) { packet.setReadPos(packet.getSize()); break; } uint64_t victimGuid = readEnergizeGuid(); - if (packet.getSize() - packet.getReadPos() < (energizeTbc ? 8u : 1u)) { + if (packet.getSize() - packet.getReadPos() < (energizeTbc ? 8u : 1u) + || (!energizeTbc && !hasFullPackedGuid(packet))) { packet.setReadPos(packet.getSize()); break; } uint64_t casterGuid = readEnergizeGuid(); From b2f03b2fe069526f3b9239ad81254e58e6607158 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 14:21:55 -0700 Subject: [PATCH 23/31] fix(combatlog): clamp attacker-state subdamage count to payload --- src/game/world_packets.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 4656f086..b8e3ad95 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -3363,15 +3363,15 @@ bool AttackerStateUpdateParser::parse(network::Packet& packet, AttackerStateUpda data.totalDamage = static_cast(packet.readUInt32()); data.subDamageCount = packet.readUInt8(); - // Cap subDamageCount: each entry is 20 bytes. If the claimed count + // Cap subDamageCount: each entry is 20 bytes. If the claimed count // exceeds what the remaining bytes can hold, a GUID was mis-parsed // (off by one byte), causing the school-mask byte to be read as count. - // In that case silently clamp to the number of full entries that fit. + // In that case clamp to the number of full entries that fit. { size_t remaining = packet.getSize() - packet.getReadPos(); size_t maxFit = remaining / 20; if (data.subDamageCount > maxFit) { - data.subDamageCount = static_cast(maxFit > 0 ? 1 : 0); + data.subDamageCount = static_cast(std::min(maxFit, 64)); } else if (data.subDamageCount > 64) { data.subDamageCount = 64; } From fb7a7bf76eb01c0b5698161842959a3c024c7845 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 14:29:09 -0700 Subject: [PATCH 24/31] fix(combatlog): enforce TBC attacker-state packet bounds --- src/game/packet_parsers_tbc.cpp | 37 +++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/game/packet_parsers_tbc.cpp b/src/game/packet_parsers_tbc.cpp index b988eb66..1343b4c5 100644 --- a/src/game/packet_parsers_tbc.cpp +++ b/src/game/packet_parsers_tbc.cpp @@ -1399,15 +1399,33 @@ bool TbcPacketParsers::parseCastFailed(network::Packet& packet, CastFailedData& // would mis-parse TBC's GUIDs and corrupt all subsequent damage fields. // ============================================================================ bool TbcPacketParsers::parseAttackerStateUpdate(network::Packet& packet, AttackerStateUpdateData& data) { - if (packet.getSize() - packet.getReadPos() < 21) return false; + data = AttackerStateUpdateData{}; - data.hitInfo = packet.readUInt32(); - data.attackerGuid = packet.readUInt64(); // full GUID in TBC - data.targetGuid = packet.readUInt64(); // full GUID in TBC - data.totalDamage = static_cast(packet.readUInt32()); + const size_t startPos = packet.getReadPos(); + auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; + + // Fixed fields before sub-damage list: + // hitInfo(4) + attackerGuid(8) + targetGuid(8) + totalDamage(4) + subDamageCount(1) = 25 bytes + if (rem() < 25) return false; + + data.hitInfo = packet.readUInt32(); + data.attackerGuid = packet.readUInt64(); // full GUID in TBC + data.targetGuid = packet.readUInt64(); // full GUID in TBC + data.totalDamage = static_cast(packet.readUInt32()); data.subDamageCount = packet.readUInt8(); + // Clamp to what can fit in the remaining payload (20 bytes per sub-damage entry). + const uint8_t maxSubDamageCount = static_cast(std::min(rem() / 20, 64)); + if (data.subDamageCount > maxSubDamageCount) { + data.subDamageCount = maxSubDamageCount; + } + + data.subDamages.reserve(data.subDamageCount); for (uint8_t i = 0; i < data.subDamageCount; ++i) { + if (rem() < 20) { + packet.setReadPos(startPos); + return false; + } SubDamage sub; sub.schoolMask = packet.readUInt32(); sub.damage = packet.readFloat(); @@ -1417,10 +1435,17 @@ bool TbcPacketParsers::parseAttackerStateUpdate(network::Packet& packet, Attacke data.subDamages.push_back(sub); } + data.subDamageCount = static_cast(data.subDamages.size()); + + // victimState + overkill are part of the expected payload. + if (rem() < 8) { + packet.setReadPos(startPos); + return false; + } data.victimState = packet.readUInt32(); data.overkill = static_cast(packet.readUInt32()); - if (packet.getReadPos() < packet.getSize()) { + if (rem() >= 4) { data.blocked = packet.readUInt32(); } From 442d3baea537f5bcd6c08c307accb196f7f6d7aa Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 14:35:57 -0700 Subject: [PATCH 25/31] fix(combatlog): reject truncated classic attacker-state packets --- src/game/packet_parsers_classic.cpp | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index 2dde86c7..2a8b5268 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -510,6 +510,8 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da // + uint32(victimState) + int32(overkill) [+ uint32(blocked)] // ============================================================================ bool ClassicPacketParsers::parseAttackerStateUpdate(network::Packet& packet, AttackerStateUpdateData& data) { + data = AttackerStateUpdateData{}; + auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; if (rem() < 5) return false; // hitInfo(4) + at least GUID mask byte(1) @@ -526,11 +528,24 @@ bool ClassicPacketParsers::parseAttackerStateUpdate(network::Packet& packet, Att } data.targetGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla - if (rem() < 5) return false; // int32 totalDamage + uint8 subDamageCount + if (rem() < 5) { + packet.setReadPos(startPos); + return false; + } // int32 totalDamage + uint8 subDamageCount data.totalDamage = static_cast(packet.readUInt32()); data.subDamageCount = packet.readUInt8(); - for (uint8_t i = 0; i < data.subDamageCount && rem() >= 20; ++i) { + const uint8_t maxSubDamageCount = static_cast(std::min(rem() / 20, 64)); + if (data.subDamageCount > maxSubDamageCount) { + data.subDamageCount = maxSubDamageCount; + } + + data.subDamages.reserve(data.subDamageCount); + for (uint8_t i = 0; i < data.subDamageCount; ++i) { + if (rem() < 20) { + packet.setReadPos(startPos); + return false; + } SubDamage sub; sub.schoolMask = packet.readUInt32(); sub.damage = packet.readFloat(); @@ -539,8 +554,12 @@ bool ClassicPacketParsers::parseAttackerStateUpdate(network::Packet& packet, Att sub.resisted = packet.readUInt32(); data.subDamages.push_back(sub); } + data.subDamageCount = static_cast(data.subDamages.size()); - if (rem() < 8) return true; + if (rem() < 8) { + packet.setReadPos(startPos); + return false; + } data.victimState = packet.readUInt32(); data.overkill = static_cast(packet.readUInt32()); From c7dffccb4e453aac25d3bc184f38acae9bcf3aca Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 14:43:15 -0700 Subject: [PATCH 26/31] fix(combatlog): reject spell start packets missing target flags --- src/game/packet_parsers_classic.cpp | 24 ++++++++++++++++++++---- src/game/packet_parsers_tbc.cpp | 23 +++++++++++++++-------- src/game/world_packets.cpp | 26 ++++++++++++++++---------- 3 files changed, 51 insertions(+), 22 deletions(-) diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index 2a8b5268..74935162 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -355,12 +355,21 @@ network::Packet ClassicPacketParsers::buildUseItem(uint8_t bagIndex, uint8_t slo // + uint16(targetFlags) [+ PackedGuid(unitTarget) if TARGET_FLAG_UNIT] // ============================================================================ bool ClassicPacketParsers::parseSpellStart(network::Packet& packet, SpellStartData& data) { + data = SpellStartData{}; + auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; + const size_t startPos = packet.getReadPos(); if (rem() < 2) return false; - if (!hasFullPackedGuid(packet)) return false; + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; + } data.casterGuid = UpdateObjectParser::readPackedGuid(packet); - if (!hasFullPackedGuid(packet)) return false; + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; + } data.casterUnit = UpdateObjectParser::readPackedGuid(packet); // uint8 castCount + uint32 spellId + uint16 castFlags + uint32 castTime = 11 bytes @@ -371,11 +380,18 @@ bool ClassicPacketParsers::parseSpellStart(network::Packet& packet, SpellStartDa data.castTime = packet.readUInt32(); // SpellCastTargets: uint16 targetFlags in Vanilla (uint32 in TBC/WotLK) - if (rem() < 2) return true; + if (rem() < 2) { + LOG_WARNING("[Classic] Spell start: missing targetFlags"); + packet.setReadPos(startPos); + return false; + } uint16_t targetFlags = packet.readUInt16(); // TARGET_FLAG_UNIT (0x02) or TARGET_FLAG_OBJECT (0x800) carry a packed GUID if ((targetFlags & 0x02) || (targetFlags & 0x800)) { - if (!hasFullPackedGuid(packet)) return false; + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; + } data.targetGuid = UpdateObjectParser::readPackedGuid(packet); } diff --git a/src/game/packet_parsers_tbc.cpp b/src/game/packet_parsers_tbc.cpp index 1343b4c5..d22b0584 100644 --- a/src/game/packet_parsers_tbc.cpp +++ b/src/game/packet_parsers_tbc.cpp @@ -1234,6 +1234,8 @@ bool TbcPacketParsers::parseMailList(network::Packet& packet, std::vector packet.getSize()) { - return false; - } - data.targetGuid = packet.readUInt64(); // full GUID in TBC + if (packet.getReadPos() + 4 > packet.getSize()) { + LOG_WARNING("[TBC] Spell start: missing targetFlags"); + packet.setReadPos(startPos); + return false; + } + + uint32_t targetFlags = packet.readUInt32(); + const bool needsTargetGuid = (targetFlags & 0x02) || (targetFlags & 0x800); // UNIT/OBJECT + if (needsTargetGuid) { + if (packet.getReadPos() + 8 > packet.getSize()) { + packet.setReadPos(startPos); + return false; } + data.targetGuid = packet.readUInt64(); // full GUID in TBC } LOG_DEBUG("[TBC] Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms"); diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index b8e3ad95..f5da2e5f 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -3703,6 +3703,8 @@ bool CastFailedParser::parse(network::Packet& packet, CastFailedData& data) { } bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) { + data = SpellStartData{}; + // Packed GUIDs are variable-length; only require minimal packet shape up front: // two GUID masks + castCount(1) + spellId(4) + castFlags(4) + castTime(4). if (packet.getSize() - packet.getReadPos() < 15) return false; @@ -3729,17 +3731,21 @@ bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) { data.castFlags = packet.readUInt32(); data.castTime = packet.readUInt32(); - // Read target flags and target (simplified) - if (packet.getSize() - packet.getReadPos() >= 4) { - uint32_t targetFlags = packet.readUInt32(); - const bool needsTargetGuid = (targetFlags & 0x02) || (targetFlags & 0x800); // UNIT/OBJECT - if (needsTargetGuid) { - if (!hasFullPackedGuid(packet)) { - packet.setReadPos(startPos); - return false; - } - data.targetGuid = UpdateObjectParser::readPackedGuid(packet); + // SpellCastTargets starts with target flags and is mandatory. + if (packet.getSize() - packet.getReadPos() < 4) { + LOG_WARNING("Spell start: missing targetFlags"); + packet.setReadPos(startPos); + return false; + } + + uint32_t targetFlags = packet.readUInt32(); + const bool needsTargetGuid = (targetFlags & 0x02) || (targetFlags & 0x800); // UNIT/OBJECT + if (needsTargetGuid) { + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(startPos); + return false; } + data.targetGuid = UpdateObjectParser::readPackedGuid(packet); } LOG_DEBUG("Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms"); From f930ecbffd5111e9c794d0bb5c236e6f547a235f Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 14:51:27 -0700 Subject: [PATCH 27/31] fix(combatlog): reject truncated instakill logs without spell id --- src/game/game_handler.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index f629002c..4780c372 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -6527,7 +6527,10 @@ void GameHandler::handlePacket(network::Packet& packet) { } uint64_t ikVictim = ikUsesFullGuid ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - uint32_t ikSpell = (ik_rem() >= 4) ? packet.readUInt32() : 0; + if (ik_rem() < 4) { + packet.setReadPos(packet.getSize()); break; + } + uint32_t ikSpell = packet.readUInt32(); // Show kill/death feedback for the local player if (ikCaster == playerGuid) { addCombatText(CombatTextEntry::INSTAKILL, 0, ikSpell, true, 0, ikCaster, ikVictim); From a42428d11771f01f9c99989862a0046deab048d7 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 14:59:24 -0700 Subject: [PATCH 28/31] fix(combatlog): accept extended TBC spell damage payloads --- src/game/packet_parsers_tbc.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/game/packet_parsers_tbc.cpp b/src/game/packet_parsers_tbc.cpp index d22b0584..308873f1 100644 --- a/src/game/packet_parsers_tbc.cpp +++ b/src/game/packet_parsers_tbc.cpp @@ -1472,11 +1472,12 @@ bool TbcPacketParsers::parseSpellDamageLog(network::Packet& packet, SpellDamageL // targetGuid(8) + attackerGuid(8) + spellId(4) + damage(4) + schoolMask(1) // + absorbed(4) + resisted(4) + periodicLog(1) + unused(1) + blocked(4) + flags(4) // = 43 bytes + // Some servers append additional trailing fields; consume the canonical minimum + // and leave any extension bytes unread. if (packet.getSize() - packet.getReadPos() < 43) return false; data = SpellDamageLogData{}; - const size_t startPos = packet.getReadPos(); data.targetGuid = packet.readUInt64(); // full GUID in TBC data.attackerGuid = packet.readUInt64(); // full GUID in TBC data.spellId = packet.readUInt32(); @@ -1495,11 +1496,6 @@ bool TbcPacketParsers::parseSpellDamageLog(network::Packet& packet, SpellDamageL // TBC does not have an overkill field here data.overkill = 0; - if (packet.getReadPos() - startPos != 43) { - packet.setReadPos(startPos); - return false; - } - LOG_DEBUG("[TBC] Spell damage: spellId=", data.spellId, " dmg=", data.damage, data.isCrit ? " CRIT" : ""); return true; From 73f38eaa76f15ad348428567bd9af37c5161e179 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 15:06:29 -0700 Subject: [PATCH 29/31] fix(combatlog): reject truncated resist logs --- src/game/game_handler.cpp | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 4780c372..40c4d34b 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -7146,19 +7146,18 @@ void GameHandler::handlePacket(network::Packet& packet) { ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); if (rl_rem() < 4) { packet.setReadPos(packet.getSize()); break; } uint32_t spellId = packet.readUInt32(); - int32_t resistedAmount = 0; // Resist payload includes: // float resistFactor + uint32 targetResistance + uint32 resistedValue. - // Some servers may truncate optional tail fields, so parse defensively. - if (rl_rem() >= 12) { - /*float resistFactor =*/ packet.readFloat(); - /*uint32_t targetRes =*/ packet.readUInt32(); - resistedAmount = static_cast(packet.readUInt32()); - } + // Require the full payload so truncated packets cannot synthesize + // zero-value resist events. + if (rl_rem() < 12) { packet.setReadPos(packet.getSize()); break; } + /*float resistFactor =*/ packet.readFloat(); + /*uint32_t targetRes =*/ packet.readUInt32(); + int32_t resistedAmount = static_cast(packet.readUInt32()); // Show RESIST when the player is involved on either side. - if (victimGuid == playerGuid) { + if (resistedAmount > 0 && victimGuid == playerGuid) { addCombatText(CombatTextEntry::RESIST, resistedAmount, spellId, false, 0, attackerGuid, victimGuid); - } else if (attackerGuid == playerGuid) { + } else if (resistedAmount > 0 && attackerGuid == playerGuid) { addCombatText(CombatTextEntry::RESIST, resistedAmount, spellId, true, 0, attackerGuid, victimGuid); } packet.setReadPos(packet.getSize()); From 98acf28bee0f827dc7a240f0ca08ef24774cf515 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 15:13:49 -0700 Subject: [PATCH 30/31] fix(combatlog): reject truncated spell damage log tails --- src/game/world_packets.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index f5da2e5f..00229c0c 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -3423,8 +3423,11 @@ bool AttackerStateUpdateParser::parse(network::Packet& packet, AttackerStateUpda } bool SpellDamageLogParser::parse(network::Packet& packet, SpellDamageLogData& data) { - // Upfront validation: packed GUIDs(1-8 each) + spellId(4) + damage(4) + overkill(4) + schoolMask(1) + absorbed(4) + resisted(4) = 30 bytes minimum - if (packet.getSize() - packet.getReadPos() < 30) return false; + // Upfront validation: + // packed GUIDs(1-8 each) + spellId(4) + damage(4) + overkill(4) + schoolMask(1) + // + absorbed(4) + resisted(4) + periodicLog(1) + unused(1) + blocked(4) + flags(4) + // = 33 bytes minimum. + if (packet.getSize() - packet.getReadPos() < 33) return false; size_t startPos = packet.getReadPos(); if (!hasFullPackedGuid(packet)) { @@ -3451,11 +3454,11 @@ bool SpellDamageLogParser::parse(network::Packet& packet, SpellDamageLogData& da data.absorbed = packet.readUInt32(); data.resisted = packet.readUInt32(); - // Skip remaining fields (periodicLog + unused + blocked + flags = 10 bytes) + // Remaining fields are required for a complete event. + // Reject truncated packets so we do not emit partial/incorrect combat entries. if (packet.getSize() - packet.getReadPos() < 10) { - LOG_WARNING("SpellDamageLog: truncated trailing fields"); - data.isCrit = false; - return true; + packet.setReadPos(startPos); + return false; } uint8_t periodicLog = packet.readUInt8(); From 2aebf3dd2f880b7382c4f72c4040775e0a04c7f9 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 21:49:30 -0700 Subject: [PATCH 31/31] fix: reject malformed monster move payloads --- src/game/world_packets.cpp | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 00229c0c..23efb3bc 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -3172,12 +3172,10 @@ bool MonsterMoveParser::parse(network::Packet& packet, MonsterMoveData& data) { if (pointCount == 0) return true; - // Cap pointCount to prevent excessive iteration from malformed packets + // Reject extreme point counts from malformed packets. constexpr uint32_t kMaxSplinePoints = 1000; if (pointCount > kMaxSplinePoints) { - LOG_WARNING("SMSG_MONSTER_MOVE: pointCount=", pointCount, " exceeds max ", kMaxSplinePoints, - " (guid=0x", std::hex, data.guid, std::dec, "), capping"); - pointCount = kMaxSplinePoints; + return false; } // Catmullrom or Flying → all waypoints stored as absolute float3 (uncompressed). @@ -3185,20 +3183,27 @@ bool MonsterMoveParser::parse(network::Packet& packet, MonsterMoveData& data) { bool uncompressed = (data.splineFlags & (0x00080000 | 0x00002000)) != 0; if (uncompressed) { + const size_t requiredBytes = static_cast(pointCount) * 12ull; + if (packet.getReadPos() + requiredBytes > packet.getSize()) return false; + // Read last point as destination // Skip to last point: each point is 12 bytes for (uint32_t i = 0; i < pointCount - 1; i++) { - if (packet.getReadPos() + 12 > packet.getSize()) return true; + if (packet.getReadPos() + 12 > packet.getSize()) return false; packet.readFloat(); packet.readFloat(); packet.readFloat(); } - if (packet.getReadPos() + 12 > packet.getSize()) return true; + if (packet.getReadPos() + 12 > packet.getSize()) return false; data.destX = packet.readFloat(); data.destY = packet.readFloat(); data.destZ = packet.readFloat(); data.hasDest = true; } else { // Compressed: first 3 floats are the destination (final point) - if (packet.getReadPos() + 12 > packet.getSize()) return true; + size_t requiredBytes = 12; + if (pointCount > 1) { + requiredBytes += static_cast(pointCount - 1) * 4ull; + } + if (packet.getReadPos() + requiredBytes > packet.getSize()) return false; data.destX = packet.readFloat(); data.destY = packet.readFloat(); data.destZ = packet.readFloat(); @@ -3282,16 +3287,19 @@ bool MonsterMoveParser::parseVanilla(network::Packet& packet, MonsterMoveData& d if (pointCount == 0) return true; - // Cap pointCount to prevent excessive iteration from malformed packets + // Reject extreme point counts from malformed packets. constexpr uint32_t kMaxSplinePoints = 1000; if (pointCount > kMaxSplinePoints) { - LOG_WARNING("SMSG_MONSTER_MOVE(Vanilla): pointCount=", pointCount, " exceeds max ", kMaxSplinePoints, - " (guid=0x", std::hex, data.guid, std::dec, "), capping"); - pointCount = kMaxSplinePoints; + return false; } + size_t requiredBytes = 12; + if (pointCount > 1) { + requiredBytes += static_cast(pointCount - 1) * 4ull; + } + if (packet.getReadPos() + requiredBytes > packet.getSize()) return false; + // First float[3] is destination. - if (packet.getReadPos() + 12 > packet.getSize()) return true; data.destX = packet.readFloat(); data.destY = packet.readFloat(); data.destZ = packet.readFloat(); @@ -3301,9 +3309,8 @@ bool MonsterMoveParser::parseVanilla(network::Packet& packet, MonsterMoveData& d if (pointCount > 1) { size_t skipBytes = static_cast(pointCount - 1) * 4; size_t newPos = packet.getReadPos() + skipBytes; - if (newPos <= packet.getSize()) { - packet.setReadPos(newPos); - } + if (newPos > packet.getSize()) return false; + packet.setReadPos(newPos); } LOG_DEBUG("MonsterMove(turtle): guid=0x", std::hex, data.guid, std::dec,