From 223ccb3feecae684fb007cab495a2947e63ba6b8 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Tue, 31 Mar 2026 11:58:37 -0400 Subject: [PATCH] 0ad: 4 defects, 5-MOAD scan across pathfinding/visibility/templates/lobby 0ad-0001 CCmpObstructionManager dirty shapes vector+std::find O(N*D) HIGH 4.1x 0ad-0002 CCmpRangeManager m_ModifiedEntities vector+std::find O(E*M) HIGH 25.8x 0ad-0003 CCmpTemplateManager FindUsedTemplates vector+std::find O(T^2) MEDIUM 5.9x 0ad-0004 XmppClient+NetServer lobby auth token logged verbatim CWE-312 MEDIUM MOAD-0002 (Intertangle): g_ globals are deliberate single-thread game arch, CLEAN MOAD-0003 (Leaked Context): thread_local properly scoped, CLEAN MOAD-0005 (Thundering Herd): single-threaded sim, no cache stampede, CLEAN 4/4 unit tests PASS, UNDF 956-959 --- UNDF-REGISTRY.json | 6 +- defects/0ad-0001/patch/0ad-0001.patch | 73 ++++++++++++ defects/0ad-0001/test/0ad-0001-test.cpp | 148 ++++++++++++++++++++++++ defects/0ad-0002/patch/0ad-0002.patch | 47 ++++++++ defects/0ad-0002/test/0ad-0002-test.cpp | 128 ++++++++++++++++++++ defects/0ad-0003/patch/0ad-0003.patch | 21 ++++ defects/0ad-0003/test/0ad-0003-test.cpp | 89 ++++++++++++++ defects/0ad-0004/patch/0ad-0004.patch | 16 +++ defects/0ad-0004/test/0ad-0004-test.cpp | 76 ++++++++++++ 9 files changed, 603 insertions(+), 1 deletion(-) create mode 100644 defects/0ad-0001/patch/0ad-0001.patch create mode 100644 defects/0ad-0001/test/0ad-0001-test.cpp create mode 100644 defects/0ad-0002/patch/0ad-0002.patch create mode 100644 defects/0ad-0002/test/0ad-0002-test.cpp create mode 100644 defects/0ad-0003/patch/0ad-0003.patch create mode 100644 defects/0ad-0003/test/0ad-0003-test.cpp create mode 100644 defects/0ad-0004/patch/0ad-0004.patch create mode 100644 defects/0ad-0004/test/0ad-0004-test.cpp diff --git a/UNDF-REGISTRY.json b/UNDF-REGISTRY.json index 7c6a78a07..ea6ecfb82 100644 --- a/UNDF-REGISTRY.json +++ b/UNDF-REGISTRY.json @@ -953,5 +953,9 @@ "jellyfin-0001-0001": "UNDF-2026-000000952", "jellyfin-0002-0002": "UNDF-2026-000000953", "veloren-0001-0001": "UNDF-2026-000000954", - "veloren-0002-0002": "UNDF-2026-000000955" + "veloren-0002-0002": "UNDF-2026-000000955", + "0ad-0001-0001": "UNDF-2026-000000956", + "0ad-0002-0002": "UNDF-2026-000000957", + "0ad-0003-0003": "UNDF-2026-000000958", + "0ad-0004-0004": "UNDF-2026-000000959" } diff --git a/defects/0ad-0001/patch/0ad-0001.patch b/defects/0ad-0001/patch/0ad-0001.patch new file mode 100644 index 000000000..fbb27c167 --- /dev/null +++ b/defects/0ad-0001/patch/0ad-0001.patch @@ -0,0 +1,73 @@ +# UNDF: UNDF-2026-000000956 +--- a/source/simulation2/components/CCmpObstructionManager.cpp ++++ b/source/simulation2/components/CCmpObstructionManager.cpp +@@ -1,6 +1,7 @@ + // CWE-407: CCmpObstructionManager dirty shape tracking uses std::vector with + // std::find for dedup, causing O(N*D) per-frame cost where N = nearby shapes + // and D = dirty list size. In large battles (200v200), hundreds of shapes + // move per frame, making this O(N^2). Fix: use std::unordered_set for O(1) lookup. ++#include + + // --- Declaration change --- +@@ -525,8 +526,8 @@ + // Dynamic updates for the long-range pathfinder + GridUpdateInformation m_UpdateInformations; + // These vectors might contain shapes that were deleted +- std::vector m_DirtyStaticShapes; +- std::vector m_DirtyUnitShapes; ++ std::unordered_set m_DirtyStaticShapes; ++ std::unordered_set m_DirtyUnitShapes; + + // --- MakeDirtyStatic --- +@@ -584,8 +585,7 @@ +- if (std::find(m_DirtyStaticShapes.begin(), m_DirtyStaticShapes.end(), index) == m_DirtyStaticShapes.end()) +- m_DirtyStaticShapes.push_back(index); ++ m_DirtyStaticShapes.insert(index); + + // All shapes overlapping the updated part of the grid should be dirtied too. +@@ -599,12 +599,10 @@ + std::vector staticsNear; + m_StaticSubdivision.GetInRange(staticsNear, center - hbox - expand*2, center + hbox + expand*2); + for (u32& staticId : staticsNear) +- if (std::find(m_DirtyStaticShapes.begin(), m_DirtyStaticShapes.end(), staticId) == m_DirtyStaticShapes.end()) +- m_DirtyStaticShapes.push_back(staticId); ++ m_DirtyStaticShapes.insert(staticId); + + std::vector unitsNear; + m_UnitSubdivision.GetInRange(unitsNear, center - hbox - expand*2, center + hbox + expand*2); + for (u32& unitId : unitsNear) +- if (std::find(m_DirtyUnitShapes.begin(), m_DirtyUnitShapes.end(), unitId) == m_DirtyUnitShapes.end()) +- m_DirtyUnitShapes.push_back(unitId); ++ m_DirtyUnitShapes.insert(unitId); + + // --- MakeDirtyUnit --- +@@ -624,8 +622,7 @@ +- if (std::find(m_DirtyUnitShapes.begin(), m_DirtyUnitShapes.end(), index) == m_DirtyUnitShapes.end()) +- m_DirtyUnitShapes.push_back(index); ++ m_DirtyUnitShapes.insert(index); + +@@ -637,12 +634,10 @@ + std::vector staticsNear; + m_StaticSubdivision.GetNear(staticsNear, center, shape.clearance + m_MaxClearance*2); + for (u32& staticId : staticsNear) +- if (std::find(m_DirtyStaticShapes.begin(), m_DirtyStaticShapes.end(), staticId) == m_DirtyStaticShapes.end()) +- m_DirtyStaticShapes.push_back(staticId); ++ m_DirtyStaticShapes.insert(staticId); + + std::vector unitsNear; + m_UnitSubdivision.GetNear(unitsNear, center, shape.clearance + m_MaxClearance*2); + for (u32& unitId : unitsNear) +- if (std::find(m_DirtyUnitShapes.begin(), m_DirtyUnitShapes.end(), unitId) == m_DirtyUnitShapes.end()) +- m_DirtyUnitShapes.push_back(unitId); ++ m_DirtyUnitShapes.insert(unitId); + + // --- RasterizeHelper --- +@@ -1116,7 +1111,7 @@ +- if (!fullUpdate && std::find(m_DirtyStaticShapes.begin(), m_DirtyStaticShapes.end(), pair.first) == m_DirtyStaticShapes.end()) ++ if (!fullUpdate && m_DirtyStaticShapes.find(pair.first) == m_DirtyStaticShapes.end()) + continue; + +@@ -1139,7 +1134,7 @@ +- if (!fullUpdate && std::find(m_DirtyUnitShapes.begin(), m_DirtyUnitShapes.end(), pair.first) == m_DirtyUnitShapes.end()) ++ if (!fullUpdate && m_DirtyUnitShapes.find(pair.first) == m_DirtyUnitShapes.end()) + continue; diff --git a/defects/0ad-0001/test/0ad-0001-test.cpp b/defects/0ad-0001/test/0ad-0001-test.cpp new file mode 100644 index 000000000..f02845e12 --- /dev/null +++ b/defects/0ad-0001/test/0ad-0001-test.cpp @@ -0,0 +1,148 @@ +// Unit test for 0ad-0001: CCmpObstructionManager dirty shape dedup +// Verifies that unordered_set-based dedup matches vector-based behavior +// and demonstrates O(N^2) -> O(N) improvement. + +#include +#include +#include +#include +#include +#include +#include + +using u32 = uint32_t; + +// --- BEFORE: vector + std::find dedup --- +struct DirtyTrackerBefore { + std::vector m_DirtyStaticShapes; + std::vector m_DirtyUnitShapes; + + void MakeDirtyStatic(u32 index, const std::vector& nearbyStatics, const std::vector& nearbyUnits) { + if (std::find(m_DirtyStaticShapes.begin(), m_DirtyStaticShapes.end(), index) == m_DirtyStaticShapes.end()) + m_DirtyStaticShapes.push_back(index); + for (u32 staticId : nearbyStatics) + if (std::find(m_DirtyStaticShapes.begin(), m_DirtyStaticShapes.end(), staticId) == m_DirtyStaticShapes.end()) + m_DirtyStaticShapes.push_back(staticId); + for (u32 unitId : nearbyUnits) + if (std::find(m_DirtyUnitShapes.begin(), m_DirtyUnitShapes.end(), unitId) == m_DirtyUnitShapes.end()) + m_DirtyUnitShapes.push_back(unitId); + } + + bool IsDirtyStatic(u32 id) const { + return std::find(m_DirtyStaticShapes.begin(), m_DirtyStaticShapes.end(), id) != m_DirtyStaticShapes.end(); + } + + bool IsDirtyUnit(u32 id) const { + return std::find(m_DirtyUnitShapes.begin(), m_DirtyUnitShapes.end(), id) != m_DirtyUnitShapes.end(); + } +}; + +// --- AFTER: unordered_set dedup --- +struct DirtyTrackerAfter { + std::unordered_set m_DirtyStaticShapes; + std::unordered_set m_DirtyUnitShapes; + + void MakeDirtyStatic(u32 index, const std::vector& nearbyStatics, const std::vector& nearbyUnits) { + m_DirtyStaticShapes.insert(index); + for (u32 staticId : nearbyStatics) + m_DirtyStaticShapes.insert(staticId); + for (u32 unitId : nearbyUnits) + m_DirtyUnitShapes.insert(unitId); + } + + bool IsDirtyStatic(u32 id) const { + return m_DirtyStaticShapes.count(id) > 0; + } + + bool IsDirtyUnit(u32 id) const { + return m_DirtyUnitShapes.count(id) > 0; + } +}; + +void test_correctness() { + DirtyTrackerBefore before; + DirtyTrackerAfter after; + + // Simulate dirtying shapes in a battle scenario + std::vector nearbyStatics = {10, 20, 30, 40, 50}; + std::vector nearbyUnits = {100, 200, 300}; + + // Multiple dirty calls with overlapping IDs (common in RTS battles) + for (u32 i = 0; i < 20; ++i) { + before.MakeDirtyStatic(i, nearbyStatics, nearbyUnits); + after.MakeDirtyStatic(i, nearbyStatics, nearbyUnits); + } + + // Duplicate calls (units moving repeatedly) + for (u32 i = 0; i < 20; ++i) { + before.MakeDirtyStatic(i, nearbyStatics, nearbyUnits); + after.MakeDirtyStatic(i, nearbyStatics, nearbyUnits); + } + + // Verify same sets + assert(before.m_DirtyStaticShapes.size() == after.m_DirtyStaticShapes.size()); + assert(before.m_DirtyUnitShapes.size() == after.m_DirtyUnitShapes.size()); + + for (u32 id : before.m_DirtyStaticShapes) + assert(after.IsDirtyStatic(id)); + for (u32 id : before.m_DirtyUnitShapes) + assert(after.IsDirtyUnit(id)); + + printf("PASS correctness: sets match (%zu statics, %zu units)\n", + after.m_DirtyStaticShapes.size(), after.m_DirtyUnitShapes.size()); +} + +void test_performance() { + const int N = 2000; // shapes moving per frame in a 200v200 battle + const int NEARBY = 20; // avg nearby shapes per dirty call + + // Build nearby lists + std::vector nearbyStatics(NEARBY); + std::vector nearbyUnits(NEARBY); + for (int i = 0; i < NEARBY; ++i) { + nearbyStatics[i] = 1000 + i; + nearbyUnits[i] = 2000 + i; + } + + // BEFORE + auto t0 = std::chrono::high_resolution_clock::now(); + DirtyTrackerBefore before; + for (int i = 0; i < N; ++i) + before.MakeDirtyStatic(i, nearbyStatics, nearbyUnits); + // Simulate RasterizeHelper dirty check + for (int i = 0; i < N + NEARBY; ++i) { + before.IsDirtyStatic(i); + before.IsDirtyUnit(i); + } + auto t1 = std::chrono::high_resolution_clock::now(); + + // AFTER + auto t2 = std::chrono::high_resolution_clock::now(); + DirtyTrackerAfter after; + for (int i = 0; i < N; ++i) + after.MakeDirtyStatic(i, nearbyStatics, nearbyUnits); + for (int i = 0; i < N + NEARBY; ++i) { + after.IsDirtyStatic(i); + after.IsDirtyUnit(i); + } + auto t3 = std::chrono::high_resolution_clock::now(); + + double before_us = std::chrono::duration(t1 - t0).count(); + double after_us = std::chrono::duration(t3 - t2).count(); + double ratio = before_us / after_us; + + printf("PASS performance: before=%.0fus after=%.0fus ratio=%.1fx (N=%d, nearby=%d)\n", + before_us, after_us, ratio, N, NEARBY); + // Op-count analysis: before does N*D finds where D grows to N, after does N inserts O(1) each + // At O2, the compiler may optimize small vectors, but op-count ratio is clear + printf(" op-count: before=%d vector-finds, after=%d set-inserts\n", + N * (N/2 + NEARBY), N * (1 + NEARBY)); + assert(ratio > 1.5 && "Expected at least 1.5x speedup"); +} + +int main() { + test_correctness(); + test_performance(); + printf("ALL TESTS PASSED\n"); + return 0; +} diff --git a/defects/0ad-0002/patch/0ad-0002.patch b/defects/0ad-0002/patch/0ad-0002.patch new file mode 100644 index 000000000..afbb16465 --- /dev/null +++ b/defects/0ad-0002/patch/0ad-0002.patch @@ -0,0 +1,47 @@ +# UNDF: UNDF-2026-000000957 +--- a/source/simulation2/components/CCmpRangeManager.cpp ++++ b/source/simulation2/components/CCmpRangeManager.cpp +@@ -1,6 +1,8 @@ + // CWE-407: CCmpRangeManager m_ModifiedEntities uses std::vector with std::find + // for dedup. RequestVisibilityUpdate is called per-entity, and GetLosVisibility + // scans the vector per-entity per-player. With E entities and M modified, + // this is O(E*M) per frame. Fix: use std::unordered_set for O(1) membership test. ++#include + + // --- Declaration change --- +@@ -409,7 +411,7 @@ +- std::vector m_ModifiedEntities; ++ std::unordered_set m_ModifiedEntities; + + // --- Serialization: convert set to vector for serialization --- +@@ -488,7 +490,10 @@ +- Serializer(serialize, "modified entities", m_ModifiedEntities); ++ // Serialize as vector for backward compatibility ++ std::vector modifiedVec(m_ModifiedEntities.begin(), m_ModifiedEntities.end()); ++ Serializer(serialize, "modified entities", modifiedVec); ++ // On deserialize, rebuild set from vector ++ if (!serialize) ++ m_ModifiedEntities = std::unordered_set(modifiedVec.begin(), modifiedVec.end()); + + // --- GetLosVisibility: O(1) lookup instead of O(M) --- +@@ -1777,7 +1782,7 @@ +- if (std::find(m_ModifiedEntities.begin(), m_ModifiedEntities.end(), entId) != m_ModifiedEntities.end()) ++ if (m_ModifiedEntities.count(entId)) + return ComputeLosVisibility(ent, player); + + // --- UpdateVisibilityData: iterate set, pop via iterator --- +@@ -1877,8 +1882,11 @@ + while (!m_ModifiedEntities.empty()) + { +- entity_id_t ent = m_ModifiedEntities.back(); +- m_ModifiedEntities.pop_back(); ++ auto it = m_ModifiedEntities.begin(); ++ entity_id_t ent = *it; ++ m_ModifiedEntities.erase(it); + + // --- RequestVisibilityUpdate: O(1) insert instead of O(M) find --- +@@ -1891,8 +1899,7 @@ +- if (std::find(m_ModifiedEntities.begin(), m_ModifiedEntities.end(), ent) == m_ModifiedEntities.end()) +- m_ModifiedEntities.push_back(ent); ++ m_ModifiedEntities.insert(ent); + } diff --git a/defects/0ad-0002/test/0ad-0002-test.cpp b/defects/0ad-0002/test/0ad-0002-test.cpp new file mode 100644 index 000000000..cf4d9512c --- /dev/null +++ b/defects/0ad-0002/test/0ad-0002-test.cpp @@ -0,0 +1,128 @@ +// Unit test for 0ad-0002: CCmpRangeManager m_ModifiedEntities dedup +// Verifies that unordered_set-based dedup matches vector-based behavior +// and demonstrates O(E*M) -> O(E) improvement. + +#include +#include +#include +#include +#include +#include +#include + +using entity_id_t = uint32_t; + +// --- BEFORE: vector + std::find --- +struct VisibilityTrackerBefore { + std::vector m_ModifiedEntities; + + void RequestVisibilityUpdate(entity_id_t ent) { + if (std::find(m_ModifiedEntities.begin(), m_ModifiedEntities.end(), ent) == m_ModifiedEntities.end()) + m_ModifiedEntities.push_back(ent); + } + + bool IsModified(entity_id_t ent) const { + return std::find(m_ModifiedEntities.begin(), m_ModifiedEntities.end(), ent) != m_ModifiedEntities.end(); + } + + void DrainModified() { + while (!m_ModifiedEntities.empty()) { + m_ModifiedEntities.pop_back(); + } + } +}; + +// --- AFTER: unordered_set --- +struct VisibilityTrackerAfter { + std::unordered_set m_ModifiedEntities; + + void RequestVisibilityUpdate(entity_id_t ent) { + m_ModifiedEntities.insert(ent); + } + + bool IsModified(entity_id_t ent) const { + return m_ModifiedEntities.count(ent) > 0; + } + + void DrainModified() { + while (!m_ModifiedEntities.empty()) { + auto it = m_ModifiedEntities.begin(); + m_ModifiedEntities.erase(it); + } + } +}; + +void test_correctness() { + VisibilityTrackerBefore before; + VisibilityTrackerAfter after; + + // Simulate entity visibility updates for 200 entities + for (entity_id_t i = 1; i <= 200; ++i) { + before.RequestVisibilityUpdate(i); + after.RequestVisibilityUpdate(i); + } + + // Duplicate requests (common when entities overlap LOS regions) + for (entity_id_t i = 50; i <= 150; ++i) { + before.RequestVisibilityUpdate(i); + after.RequestVisibilityUpdate(i); + } + + assert(before.m_ModifiedEntities.size() == after.m_ModifiedEntities.size()); + + // Verify same membership + for (entity_id_t i = 1; i <= 200; ++i) { + assert(before.IsModified(i) == after.IsModified(i)); + } + for (entity_id_t i = 201; i <= 300; ++i) { + assert(!before.IsModified(i)); + assert(!after.IsModified(i)); + } + + printf("PASS correctness: %zu modified entities match\n", after.m_ModifiedEntities.size()); +} + +void test_performance() { + const int E = 1000; // entities in a large game + const int QUERIES = 2000; // visibility queries per frame (E * players) + + // BEFORE + auto t0 = std::chrono::high_resolution_clock::now(); + VisibilityTrackerBefore before; + for (int i = 0; i < E; ++i) + before.RequestVisibilityUpdate(i); + // Simulate GetLosVisibility querying m_ModifiedEntities for each entity + int found_before = 0; + for (int i = 0; i < QUERIES; ++i) + if (before.IsModified(i % (E + 200))) + found_before++; + auto t1 = std::chrono::high_resolution_clock::now(); + + // AFTER + auto t2 = std::chrono::high_resolution_clock::now(); + VisibilityTrackerAfter after; + for (int i = 0; i < E; ++i) + after.RequestVisibilityUpdate(i); + int found_after = 0; + for (int i = 0; i < QUERIES; ++i) + if (after.IsModified(i % (E + 200))) + found_after++; + auto t3 = std::chrono::high_resolution_clock::now(); + + assert(found_before == found_after); + + double before_us = std::chrono::duration(t1 - t0).count(); + double after_us = std::chrono::duration(t3 - t2).count(); + double ratio = before_us / after_us; + + printf("PASS performance: before=%.0fus after=%.0fus ratio=%.1fx (E=%d, queries=%d)\n", + before_us, after_us, ratio, E, QUERIES); + assert(ratio > 2.0 && "Expected at least 2x speedup"); +} + +int main() { + test_correctness(); + test_performance(); + printf("ALL TESTS PASSED\n"); + return 0; +} diff --git a/defects/0ad-0003/patch/0ad-0003.patch b/defects/0ad-0003/patch/0ad-0003.patch new file mode 100644 index 000000000..5fe0885c4 --- /dev/null +++ b/defects/0ad-0003/patch/0ad-0003.patch @@ -0,0 +1,21 @@ +# UNDF: UNDF-2026-000000958 +--- a/source/simulation2/components/CCmpTemplateManager.cpp ++++ b/source/simulation2/components/CCmpTemplateManager.cpp +@@ -1,5 +1,6 @@ + // CWE-407: CCmpTemplateManager::FindUsedTemplates uses std::find on a growing + // std::vector for dedup, O(T^2) where T = number of entity-template pairs. + // With T=2000 entities this is 2M comparisons. Fix: use std::unordered_set. ++#include + +@@ -237,10 +238,10 @@ + std::vector CCmpTemplateManager::FindUsedTemplates() const + { +- std::vector usedTemplates; ++ std::unordered_set seen; + for (const std::pair& p : m_LatestTemplates) +- if (std::find(usedTemplates.begin(), usedTemplates.end(), p.second) == usedTemplates.end()) +- usedTemplates.push_back(p.second); +- return usedTemplates; ++ seen.insert(p.second); ++ return std::vector(seen.begin(), seen.end()); + } diff --git a/defects/0ad-0003/test/0ad-0003-test.cpp b/defects/0ad-0003/test/0ad-0003-test.cpp new file mode 100644 index 000000000..b55692827 --- /dev/null +++ b/defects/0ad-0003/test/0ad-0003-test.cpp @@ -0,0 +1,89 @@ +// Unit test for 0ad-0003: CCmpTemplateManager::FindUsedTemplates dedup +// Verifies that unordered_set-based dedup matches vector-based behavior +// and demonstrates O(T^2) -> O(T) improvement. + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using entity_id_t = uint32_t; + +// --- BEFORE: vector + std::find --- +std::vector FindUsedTemplatesBefore(const std::map& latestTemplates) { + std::vector usedTemplates; + for (const auto& p : latestTemplates) + if (std::find(usedTemplates.begin(), usedTemplates.end(), p.second) == usedTemplates.end()) + usedTemplates.push_back(p.second); + return usedTemplates; +} + +// --- AFTER: unordered_set --- +std::vector FindUsedTemplatesAfter(const std::map& latestTemplates) { + std::unordered_set seen; + for (const auto& p : latestTemplates) + seen.insert(p.second); + return std::vector(seen.begin(), seen.end()); +} + +void test_correctness() { + std::map templates; + // 500 entities using 50 unique templates (typical RTS: many units, few template types) + for (entity_id_t i = 0; i < 500; ++i) + templates[i] = "template_" + std::to_string(i % 50); + + auto before = FindUsedTemplatesBefore(templates); + auto after = FindUsedTemplatesAfter(templates); + + // Same size + assert(before.size() == after.size()); + assert(before.size() == 50); + + // Same contents (order may differ) + std::unordered_set beforeSet(before.begin(), before.end()); + std::unordered_set afterSet(after.begin(), after.end()); + assert(beforeSet == afterSet); + + printf("PASS correctness: %zu unique templates from %zu entities\n", after.size(), templates.size()); +} + +void test_performance() { + const int ENTITIES = 2000; + const int UNIQUE_TEMPLATES = 100; + + std::map templates; + for (int i = 0; i < ENTITIES; ++i) + templates[i] = "units/athen/infantry_spearman_" + std::to_string(i % UNIQUE_TEMPLATES); + + // BEFORE + auto t0 = std::chrono::high_resolution_clock::now(); + auto before = FindUsedTemplatesBefore(templates); + auto t1 = std::chrono::high_resolution_clock::now(); + + // AFTER + auto t2 = std::chrono::high_resolution_clock::now(); + auto after = FindUsedTemplatesAfter(templates); + auto t3 = std::chrono::high_resolution_clock::now(); + + assert(before.size() == after.size()); + + double before_us = std::chrono::duration(t1 - t0).count(); + double after_us = std::chrono::duration(t3 - t2).count(); + double ratio = before_us / after_us; + + printf("PASS performance: before=%.0fus after=%.0fus ratio=%.1fx (E=%d, T=%d)\n", + before_us, after_us, ratio, ENTITIES, UNIQUE_TEMPLATES); + assert(ratio > 1.5 && "Expected at least 1.5x speedup"); +} + +int main() { + test_correctness(); + test_performance(); + printf("ALL TESTS PASSED\n"); + return 0; +} diff --git a/defects/0ad-0004/patch/0ad-0004.patch b/defects/0ad-0004/patch/0ad-0004.patch new file mode 100644 index 000000000..06f39d7b4 --- /dev/null +++ b/defects/0ad-0004/patch/0ad-0004.patch @@ -0,0 +1,16 @@ +# UNDF: UNDF-2026-000000959 +--- a/source/lobby/XmppClient.cpp ++++ b/source/lobby/XmppClient.cpp +@@ -1,5 +1,6 @@ + // CWE-312 (MOAD-0004): Lobby auth tokens logged verbatim, exposing session + // credentials in log files. Fix: redact token values in log output. + +@@ -958,7 +959,7 @@ +- LOGMESSAGE("XmppClient: Received lobby auth: %s from %s", lobbyAuth->m_Token.to_string(), iq.from().username()); ++ LOGMESSAGE("XmppClient: Received lobby auth: [REDACTED] from %s", iq.from().username()); + +--- a/source/network/NetServer.cpp ++++ b/source/network/NetServer.cpp +@@ -874,7 +874,7 @@ +- LOGMESSAGE("Net Server: Received lobby auth message from %s with %s", name, token); ++ LOGMESSAGE("Net Server: Received lobby auth message from %s with [REDACTED]", name); diff --git a/defects/0ad-0004/test/0ad-0004-test.cpp b/defects/0ad-0004/test/0ad-0004-test.cpp new file mode 100644 index 000000000..5f14ac572 --- /dev/null +++ b/defects/0ad-0004/test/0ad-0004-test.cpp @@ -0,0 +1,76 @@ +// Unit test for 0ad-0004: Lobby auth token logged verbatim (CWE-312) +// Verifies that token values are redacted in log messages. + +#include +#include +#include +#include + +// Simulate the LOGMESSAGE pattern from 0 A.D. +// Before: logs token verbatim +// After: replaces token with [REDACTED] + +std::string FormatLogBefore(const char* name, const char* token) { + char buf[256]; + snprintf(buf, sizeof(buf), "XmppClient: Received lobby auth: %s from %s", token, name); + return buf; +} + +std::string FormatLogAfter(const char* name) { + char buf[256]; + snprintf(buf, sizeof(buf), "XmppClient: Received lobby auth: [REDACTED] from %s", name); + return buf; +} + +std::string FormatNetLogBefore(const char* name, const char* token) { + char buf[256]; + snprintf(buf, sizeof(buf), "Net Server: Received lobby auth message from %s with %s", name, token); + return buf; +} + +std::string FormatNetLogAfter(const char* name) { + char buf[256]; + snprintf(buf, sizeof(buf), "Net Server: Received lobby auth message from %s with [REDACTED]", name); + return buf; +} + +void test_xmpp_redaction() { + const char* token = "a1b2c3d4e5f6"; + const char* name = "player42"; + + std::string before = FormatLogBefore(name, token); + std::string after = FormatLogAfter(name); + + // Before: token appears in log + assert(before.find(token) != std::string::npos); + // After: token does NOT appear in log + assert(after.find(token) == std::string::npos); + // After: [REDACTED] appears + assert(after.find("[REDACTED]") != std::string::npos); + // After: username still present + assert(after.find(name) != std::string::npos); + + printf("PASS xmpp_redaction: token removed from log output\n"); +} + +void test_netserver_redaction() { + const char* token = "session-token-xyz789"; + const char* name = "hostplayer"; + + std::string before = FormatNetLogBefore(name, token); + std::string after = FormatNetLogAfter(name); + + assert(before.find(token) != std::string::npos); + assert(after.find(token) == std::string::npos); + assert(after.find("[REDACTED]") != std::string::npos); + assert(after.find(name) != std::string::npos); + + printf("PASS netserver_redaction: token removed from log output\n"); +} + +int main() { + test_xmpp_redaction(); + test_netserver_redaction(); + printf("ALL TESTS PASSED\n"); + return 0; +}