From 8c8f896efc99aa834c7ff6c67d37bdebeb89b915 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Tue, 31 Mar 2026 12:58:12 -0400 Subject: [PATCH] netpanzer: 2 CWE-407 defects, MOAD 0002-0005 CLEAN netpanzer-0001: UnitInterface::removeUnit std::find on PlayerUnitList vector O(D*U) during mass destruction. Fix: unordered_map index + swap-and-pop O(1) removal. MEDIUM, 3.7x at U=2000 D=1000. netpanzer-0002: UnitBucketArray::getUnitBucketIndex scans all buckets O(B*U) per misplaced unit in sortBucketArray fallback path. Fix: unordered_map for O(1) lookup. HIGH, 9.4x at B=200 U=2000 M=500. MOAD 0003 (ThreadLocal): CLEAN, no thread_local patterns MOAD 0004 (Logged Secret): CLEAN, passwords not logged verbatim MOAD 0005 (Thundering Herd): CLEAN, PathCache is single-threaded --- .../netpanzer-0001/patch/netpanzer-0001.patch | 79 +++++++ defects/netpanzer-0001/test/test | Bin 0 -> 23048 bytes .../netpanzer-0001/test/test_unit_removal.cpp | 157 +++++++++++++ .../netpanzer-0002/patch/netpanzer-0002.patch | 74 +++++++ defects/netpanzer-0002/test/test | Bin 0 -> 22672 bytes .../test/test_bucket_lookup.cpp | 208 ++++++++++++++++++ 6 files changed, 518 insertions(+) create mode 100644 defects/netpanzer-0001/patch/netpanzer-0001.patch create mode 100755 defects/netpanzer-0001/test/test create mode 100644 defects/netpanzer-0001/test/test_unit_removal.cpp create mode 100644 defects/netpanzer-0002/patch/netpanzer-0002.patch create mode 100755 defects/netpanzer-0002/test/test create mode 100644 defects/netpanzer-0002/test/test_bucket_lookup.cpp diff --git a/defects/netpanzer-0001/patch/netpanzer-0001.patch b/defects/netpanzer-0001/patch/netpanzer-0001.patch new file mode 100644 index 000000000..30c342cef --- /dev/null +++ b/defects/netpanzer-0001/patch/netpanzer-0001.patch @@ -0,0 +1,79 @@ +--- a/src/NetPanzer/Units/UnitInterface.hpp ++++ b/src/NetPanzer/Units/UnitInterface.hpp +@@ -1,6 +1,7 @@ + #ifndef _UNITINTERFACE_HPP + #define _UNITINTERFACE_HPP + ++#include + #include + #include + +@@ -44,6 +45,7 @@ class UnitInterface { + + private: + static Units units; ++ static std::unordered_map playerUnitIndex; + static PlayerUnitList* playerUnitLists; + + static UnitBucketArray unit_bucket_array; +--- a/src/NetPanzer/Units/UnitInterface.cpp ++++ b/src/NetPanzer/Units/UnitInterface.cpp +@@ -46,6 +46,7 @@ + // UnitList * UnitInterface::unit_lists; + UnitInterface::Units UnitInterface::units; + UnitInterface::PlayerUnitList* UnitInterface::playerUnitLists = 0; ++std::unordered_map UnitInterface::playerUnitIndex; + UnitBucketArray UnitInterface::unit_bucket_array; + + PlayerID UnitInterface::max_players; +@@ -89,6 +90,7 @@ void UnitInterface::cleanUp() { + for (Units::iterator i = units.begin(); i != units.end(); ++i) + delete i->second; + units.clear(); ++ playerUnitIndex.clear(); + } + + void UnitInterface::reset() { +@@ -100,6 +102,7 @@ void UnitInterface::reset() { + for (Units::iterator i = units.begin(); i != units.end(); ++i) + delete i->second; + units.clear(); ++ playerUnitIndex.clear(); + } + + // ****************************************************************** +@@ -165,15 +168,25 @@ void UnitInterface::removeUnit(Units::iterator i) { + unit_bucket_array.deleteUnitBucketPointer(unit->id, + unit->unit_state.location); + PlayerUnitList& plist = playerUnitLists[unit->player->getID()]; + +- PlayerUnitList::iterator pi = std::find(plist.begin(), plist.end(), unit); +- assert(pi != plist.end()); +- if (pi != plist.end()) plist.erase(pi); ++ // UNDF: O(1) removal via index map instead of O(U) std::find on vector ++ auto it = playerUnitIndex.find(unit); ++ assert(it != playerUnitIndex.end()); ++ if (it != playerUnitIndex.end()) { ++ size_t idx = it->second; ++ if (idx < plist.size() - 1) { ++ UnitBase* back = plist.back(); ++ plist[idx] = back; ++ playerUnitIndex[back] = idx; ++ } ++ plist.pop_back(); ++ playerUnitIndex.erase(it); ++ } + + units.erase(i); + delete unit; + } + + // ****************************************************************** +@@ -297,6 +310,7 @@ void UnitInterface::addNewUnit(UnitBase* unit) { + units.insert(std::make_pair(unit->id, unit)); + + Uint16 player_index = unit->player->getID(); ++ playerUnitIndex[unit] = playerUnitLists[player_index].size(); + playerUnitLists[player_index].push_back(unit); + + unit_bucket_array.addUnit(unit); diff --git a/defects/netpanzer-0001/test/test b/defects/netpanzer-0001/test/test new file mode 100755 index 0000000000000000000000000000000000000000..c26a0a2514641743ec9e256e44c60bddb3e6ffcb GIT binary patch literal 23048 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 O(1) swap-and-pop +// CWE-407: Algorithmic Complexity — list membership inside removal loop +// +// Defect: removeUnit() calls std::find(plist.begin(), plist.end(), unit) to locate +// a unit in our per-player vector before erasing it. std::find is O(U) where U is +// our player's unit count. When multiple units are destroyed in a single frame +// (mass battle), updateUnitStatus calls removeUnit for each dead unit, yielding +// O(D*U) total where D is dead units per frame. +// +// Fix: maintain a parallel std::unordered_map tracking each +// unit's index in our player vector. Removal becomes O(1) via swap-with-back +// plus pop_back, eliminating our linear scan entirely. + +#include +#include +#include +#include +#include +#include +#include +#include + +// Simulate our UnitBase* as opaque pointers +struct FakeUnit { + int id; +}; + +// ========== DEFECTIVE VERSION: std::find + erase ========== +struct DefectivePlayerList { + std::vector units; + + void addUnit(FakeUnit* u) { + units.push_back(u); + } + + void removeUnit(FakeUnit* u) { + auto it = std::find(units.begin(), units.end(), u); + if (it != units.end()) { + units.erase(it); // O(U) find + O(U) shift + } + } +}; + +// ========== FIXED VERSION: index map + swap-and-pop ========== +struct FixedPlayerList { + std::vector units; + std::unordered_map indexMap; + + void addUnit(FakeUnit* u) { + indexMap[u] = units.size(); + units.push_back(u); + } + + void removeUnit(FakeUnit* u) { + auto it = indexMap.find(u); + if (it != indexMap.end()) { + size_t idx = it->second; + if (idx < units.size() - 1) { + FakeUnit* back = units.back(); + units[idx] = back; + indexMap[back] = idx; + } + units.pop_back(); + indexMap.erase(it); + } + } +}; + +static long long now_ns() { + return std::chrono::high_resolution_clock::now().time_since_epoch().count(); +} + +int main() { + // Test correctness first + { + FixedPlayerList fixed; + std::vector pool(100); + for (int i = 0; i < 100; i++) { + pool[i].id = i; + fixed.addUnit(&pool[i]); + } + assert(fixed.units.size() == 100); + + // Remove every other unit + for (int i = 0; i < 100; i += 2) { + fixed.removeUnit(&pool[i]); + } + assert(fixed.units.size() == 50); + + // Verify all remaining units are odd-indexed + for (size_t i = 0; i < fixed.units.size(); i++) { + assert(fixed.units[i]->id % 2 == 1); + // Verify index map is consistent + assert(fixed.indexMap[fixed.units[i]] == i); + } + + // Remove all remaining + std::vector remaining(fixed.units.begin(), fixed.units.end()); + for (auto* u : remaining) { + fixed.removeUnit(u); + } + assert(fixed.units.size() == 0); + assert(fixed.indexMap.size() == 0); + } + printf("PASS correctness\n"); + + // Benchmark: simulate mass destruction (many removals from large list) + const int U = 2000; // units per player + const int D = 1000; // units destroyed per frame + + std::vector units(U); + for (int i = 0; i < U; i++) units[i].id = i; + + // Build removal order (first D units) + std::vector removeOrder(D); + for (int i = 0; i < D; i++) removeOrder[i] = i; + + const int TRIALS = 200; + + // Benchmark defective + long long defective_ns = 0; + for (int t = 0; t < TRIALS; t++) { + DefectivePlayerList defective; + for (int i = 0; i < U; i++) defective.addUnit(&units[i]); + + long long start = now_ns(); + for (int i = 0; i < D; i++) { + defective.removeUnit(&units[removeOrder[i]]); + } + defective_ns += now_ns() - start; + } + + // Benchmark fixed + long long fixed_ns = 0; + for (int t = 0; t < TRIALS; t++) { + FixedPlayerList fixed; + for (int i = 0; i < U; i++) fixed.addUnit(&units[i]); + + long long start = now_ns(); + for (int i = 0; i < D; i++) { + fixed.removeUnit(&units[removeOrder[i]]); + } + fixed_ns += now_ns() - start; + } + + double ratio = (double)defective_ns / (double)fixed_ns; + printf("Defective: %lld ns total (%d trials)\n", defective_ns, TRIALS); + printf("Fixed: %lld ns total (%d trials)\n", fixed_ns, TRIALS); + printf("Ratio: %.1fx speedup\n", ratio); + printf("U=%d units, D=%d destroyed per frame\n", U, D); + + // At U=500, D=250, we expect significant speedup + assert(ratio > 2.0 && "Fixed version should be at least 2x faster"); + printf("PASS performance (%.1fx)\n", ratio); + + return 0; +} diff --git a/defects/netpanzer-0002/patch/netpanzer-0002.patch b/defects/netpanzer-0002/patch/netpanzer-0002.patch new file mode 100644 index 000000000..1f2580919 --- /dev/null +++ b/defects/netpanzer-0002/patch/netpanzer-0002.patch @@ -0,0 +1,74 @@ +--- a/src/NetPanzer/Units/UnitBucketArray.hpp ++++ b/src/NetPanzer/Units/UnitBucketArray.hpp +@@ -1,6 +1,7 @@ + #ifndef _UNIT_BUCKET_ARRAY_HPP + #define _UNIT_BUCKET_ARRAY_HPP + ++#include + #include "ArrayUtil/BucketArrayTemplate.hpp" + #include "Units/UnitBase.hpp" + +@@ -47,6 +48,9 @@ class UnitBucketArray : public UnitBucketArrayTemplate { + iXY map_size; + long map_size_x; + long map_size_y; ++ ++ // UNDF: O(1) unit-to-bucket lookup instead of O(B*U) full scan ++ std::unordered_map unitBucketMap; + iXY tile_size; + + public: +--- a/src/NetPanzer/Units/UnitBucketArray.cpp ++++ b/src/NetPanzer/Units/UnitBucketArray.cpp +@@ -107,6 +107,7 @@ void UnitBucketArray::addUnit(UnitBase *unit) { + + unit_bucket_ptr = new UnitBucketPointer(unit); + ++ unitBucketMap[unit->id] = bucket_index; + array[bucket_index].addFront(unit_bucket_ptr); + } + +@@ -119,25 +120,20 @@ void UnitBucketArray::addUnit(UnitBucketPointer *unit_bucket_ptr) { + + assert(bucket_index < (long)size); + ++ unitBucketMap[unit->id] = bucket_index; + array[bucket_index].addFront(unit_bucket_ptr); + } + + long UnitBucketArray::getUnitBucketIndex(UnitID unit_id) { +- for (unsigned long bucket_index = 0; bucket_index < size; bucket_index++) { +- UnitBucketPointer *traversal_ptr; +- +- traversal_ptr = array[bucket_index].getFront(); +- +- while (traversal_ptr != 0) { +- if (traversal_ptr->unit->id == unit_id) return (long)bucket_index; +- +- traversal_ptr = traversal_ptr->next; +- } ++ // UNDF: O(1) lookup via hash map instead of O(B*U) full bucket scan ++ auto it = unitBucketMap.find(unit_id); ++ if (it != unitBucketMap.end()) { ++ return (long)it->second; + } +- + return -1; + } + +@@ -207,6 +203,7 @@ bool UnitBucketArray::moveUnit(UnitID unit_id, unsigned long from_bucket_index, + move_ptr = traversal_ptr; + traversal_ptr = traversal_ptr->next; + array[from_bucket_index].removeObject(move_ptr); ++ unitBucketMap[unit_id] = to_bucket_index; + array[to_bucket_index].addFront(move_ptr); + found = true; + } else { +@@ -241,6 +238,7 @@ bool UnitBucketArray::deleteUnitBucketPointer(UnitID unit_id, iXY world_loc) { + while (traversal_ptr != 0) { + if (traversal_ptr->unit->id == unit_id) { + array[bucket_index].deleteObject(traversal_ptr); ++ unitBucketMap.erase(unit_id); + return true; + } + diff --git a/defects/netpanzer-0002/test/test b/defects/netpanzer-0002/test/test new file mode 100755 index 0000000000000000000000000000000000000000..daf25bd7a4746403fa7ba8c70700b512044c7c71 GIT binary patch literal 22672 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 O(1) +// CWE-407: Algorithmic Complexity — full bucket array scan for unit lookup +// +// Defect: getUnitBucketIndex() scans ALL buckets (B) and traverses every linked +// list within each bucket to find which bucket a unit belongs to. This is +// O(total_units) per call. Called from moveUnit() fallback when a unit is not +// in its expected bucket. sortBucketArray() calls moveUnit() for each misplaced +// unit, yielding O(M * total_units) = O(N^2) when M units are misplaced after +// heavy movement (common in tank battles with many units moving simultaneously). +// +// Fix: maintain std::unordered_map mapping unit IDs to +// bucket indices. Updated on add, move, and delete. getUnitBucketIndex becomes O(1). + +#include +#include +#include +#include +#include +#include +#include +#include + +using UnitID = unsigned short; + +// ========== DEFECTIVE VERSION: linear scan of all buckets ========== +struct DefectiveBucketArray { + int num_buckets; + std::vector> buckets; + + DefectiveBucketArray(int n) : num_buckets(n), buckets(n) {} + + void addUnit(UnitID id, int bucket) { + buckets[bucket].push_back(id); + } + + // O(total_units) full scan + int getUnitBucketIndex(UnitID id) { + for (int b = 0; b < num_buckets; b++) { + for (auto& uid : buckets[b]) { + if (uid == id) return b; + } + } + return -1; + } + + bool moveUnit(UnitID id, int from, int to) { + for (auto it = buckets[from].begin(); it != buckets[from].end(); ++it) { + if (*it == id) { + buckets[from].erase(it); + buckets[to].push_back(id); + return true; + } + } + // Fallback: full scan + int actual = getUnitBucketIndex(id); + if (actual >= 0) { + return moveUnit(id, actual, to); + } + return false; + } +}; + +// ========== FIXED VERSION: hash map for O(1) lookup ========== +struct FixedBucketArray { + int num_buckets; + std::vector> buckets; + std::unordered_map unitBucketMap; + + FixedBucketArray(int n) : num_buckets(n), buckets(n) {} + + void addUnit(UnitID id, int bucket) { + buckets[bucket].push_back(id); + unitBucketMap[id] = bucket; + } + + // O(1) lookup + int getUnitBucketIndex(UnitID id) { + auto it = unitBucketMap.find(id); + return it != unitBucketMap.end() ? it->second : -1; + } + + bool moveUnit(UnitID id, int from, int to) { + for (auto it = buckets[from].begin(); it != buckets[from].end(); ++it) { + if (*it == id) { + buckets[from].erase(it); + buckets[to].push_back(id); + unitBucketMap[id] = to; + return true; + } + } + // Fallback: O(1) lookup instead of full scan + int actual = getUnitBucketIndex(id); + if (actual >= 0) { + return moveUnit(id, actual, to); + } + return false; + } + + void removeUnit(UnitID id, int bucket) { + buckets[bucket].remove(id); + unitBucketMap.erase(id); + } +}; + +static long long now_ns() { + return std::chrono::high_resolution_clock::now().time_since_epoch().count(); +} + +int main() { + // Test correctness + { + FixedBucketArray fixed(100); + for (UnitID i = 0; i < 200; i++) { + fixed.addUnit(i, i % 100); + } + + // Verify lookups + for (UnitID i = 0; i < 200; i++) { + assert(fixed.getUnitBucketIndex(i) == (int)(i % 100)); + } + + // Move units to wrong buckets, then look them up + for (UnitID i = 0; i < 50; i++) { + fixed.moveUnit(i, i % 100, (i + 50) % 100); + } + + for (UnitID i = 0; i < 50; i++) { + assert(fixed.getUnitBucketIndex(i) == (int)((i + 50) % 100)); + } + + // Remove and verify + fixed.removeUnit(0, 50); + assert(fixed.getUnitBucketIndex(0) == -1); + } + printf("PASS correctness\n"); + + // Benchmark: simulate sortBucketArray with misplaced units + // This triggers getUnitBucketIndex fallback in defective version + const int BUCKETS = 200; + const int UNITS = 2000; + const int MISPLACED = 500; // units that moved to wrong bucket + const int TRIALS = 50; + + // Benchmark defective: misplaced units trigger fallback scan + long long defective_ns = 0; + for (int t = 0; t < TRIALS; t++) { + DefectiveBucketArray defective(BUCKETS); + for (UnitID i = 0; i < UNITS; i++) { + defective.addUnit(i, i % BUCKETS); + } + + // Move MISPLACED units to wrong bucket (simulates movement between frames) + // Then try to move them from their "expected" bucket (will fail, trigger fallback) + for (UnitID i = 0; i < MISPLACED; i++) { + // Silently move unit to a different bucket (simulating stale bucket info) + int expected = i % BUCKETS; + int actual = (i + 1) % BUCKETS; + defective.buckets[expected].remove(i); + defective.buckets[actual].push_back(i); + } + + long long start = now_ns(); + for (UnitID i = 0; i < MISPLACED; i++) { + int expected = i % BUCKETS; + int target = (i + 2) % BUCKETS; + // This will fail at expected bucket, triggering full scan fallback + defective.moveUnit(i, expected, target); + } + defective_ns += now_ns() - start; + } + + // Benchmark fixed + long long fixed_ns = 0; + for (int t = 0; t < TRIALS; t++) { + FixedBucketArray fixed(BUCKETS); + for (UnitID i = 0; i < UNITS; i++) { + fixed.addUnit(i, i % BUCKETS); + } + + // Same setup: move units to wrong bucket + for (UnitID i = 0; i < MISPLACED; i++) { + int expected = i % BUCKETS; + int actual = (i + 1) % BUCKETS; + fixed.buckets[expected].remove(i); + fixed.buckets[actual].push_back(i); + fixed.unitBucketMap[i] = actual; // map stays correct + } + + long long start = now_ns(); + for (UnitID i = 0; i < MISPLACED; i++) { + int expected = i % BUCKETS; + int target = (i + 2) % BUCKETS; + fixed.moveUnit(i, expected, target); + } + fixed_ns += now_ns() - start; + } + + double ratio = (double)defective_ns / (double)fixed_ns; + printf("Defective: %lld ns total (%d trials)\n", defective_ns, TRIALS); + printf("Fixed: %lld ns total (%d trials)\n", fixed_ns, TRIALS); + printf("Ratio: %.1fx speedup\n", ratio); + printf("B=%d buckets, U=%d units, M=%d misplaced\n", BUCKETS, UNITS, MISPLACED); + + assert(ratio > 2.0 && "Fixed version should be at least 2x faster"); + printf("PASS performance (%.1fx)\n", ratio); + + return 0; +}