diff --git a/defects/spring-rts-0001/patch/spring-rts-0001.patch b/defects/spring-rts-0001/patch/spring-rts-0001.patch new file mode 100644 index 000000000..bb3f778bb --- /dev/null +++ b/defects/spring-rts-0001/patch/spring-rts-0001.patch @@ -0,0 +1,38 @@ +--- a/rts/Sim/Weapons/Weapon.h ++++ b/rts/Sim/Weapons/Weapon.h +@@ -1,6 +1,7 @@ + #ifndef WEAPON_H + #define WEAPON_H + ++#include + #include + #include + +@@ -46,8 +47,8 @@ + virtual const float3& GetAimFromPos(bool useMuzzle = false) const { return (useMuzzle? weaponMuzzlePos: aimFromPos); } + +- bool HasIncomingProjectile(int projID) const { return (std::find(incomingProjectileIDs.begin(), incomingProjectileIDs.end(), projID) != incomingProjectileIDs.end()); } +- void AddIncomingProjectile(int projID) { incomingProjectileIDs.push_back(projID); } ++ bool HasIncomingProjectile(int projID) const { return (incomingProjectileIDs.find(projID) != incomingProjectileIDs.end()); } ++ void AddIncomingProjectile(int projID) { incomingProjectileIDs.insert(projID); } + + public: + /// test if the weapon is able to attack an enemy/mapspot just by its properties (no range check, no FreeLineOfFire check, ...) +@@ -203,7 +204,7 @@ + float3 salvoError; + + float3 errorVector; +- std::vector incomingProjectileIDs; ++ std::unordered_set incomingProjectileIDs; + + protected: + SWeaponTarget currentTarget; +--- a/rts/Sim/Weapons/Weapon.cpp ++++ b/rts/Sim/Weapons/Weapon.cpp +@@ -724,7 +724,7 @@ + // NOTE: DependentDied is called from ~CObject-->Detach, object is just barely valid + if (weaponDef->interceptor || weaponDef->isShield) { +- spring::VectorErase(incomingProjectileIDs, static_cast(o)->id); ++ incomingProjectileIDs.erase(static_cast(o)->id); + } + } diff --git a/defects/spring-rts-0001/test/test b/defects/spring-rts-0001/test/test new file mode 100755 index 000000000..af22f874b Binary files /dev/null and b/defects/spring-rts-0001/test/test differ diff --git a/defects/spring-rts-0001/test/test_intercept_handler.cpp b/defects/spring-rts-0001/test/test_intercept_handler.cpp new file mode 100644 index 000000000..2d7d59c9f --- /dev/null +++ b/defects/spring-rts-0001/test/test_intercept_handler.cpp @@ -0,0 +1,156 @@ +// Unit test for spring-rts-0001: CWeapon::HasIncomingProjectile O(I) vector scan +// in InterceptHandler::Update() nested loop = O(W * P * I) +// Fix: std::unordered_set for O(1) lookup = O(W * P) + +#include +#include +#include +#include +#include +#include + +// Simulate our BEFORE (vector-based) weapon incoming projectile tracking +struct WeaponBefore { + std::vector incomingProjectileIDs; + + bool HasIncomingProjectile(int projID) const { + return (std::find(incomingProjectileIDs.begin(), incomingProjectileIDs.end(), projID) != incomingProjectileIDs.end()); + } + void AddIncomingProjectile(int projID) { + incomingProjectileIDs.push_back(projID); + } + void RemoveIncomingProjectile(int projID) { + auto it = std::find(incomingProjectileIDs.begin(), incomingProjectileIDs.end(), projID); + if (it != incomingProjectileIDs.end()) { + *it = incomingProjectileIDs.back(); + incomingProjectileIDs.pop_back(); + } + } +}; + +// Simulate our AFTER (unordered_set-based) weapon incoming projectile tracking +struct WeaponAfter { + std::unordered_set incomingProjectileIDs; + + bool HasIncomingProjectile(int projID) const { + return (incomingProjectileIDs.find(projID) != incomingProjectileIDs.end()); + } + void AddIncomingProjectile(int projID) { + incomingProjectileIDs.insert(projID); + } + void RemoveIncomingProjectile(int projID) { + incomingProjectileIDs.erase(projID); + } +}; + +// Simulate InterceptHandler::Update() inner logic: +// for each interceptor weapon, for each interceptable projectile, +// call HasIncomingProjectile(projID) to check if already tracked +template +long long simulateInterceptUpdate( + std::vector& interceptors, + const std::vector& interceptableIDs, + int iterations +) { + auto t0 = std::chrono::high_resolution_clock::now(); + int dummy = 0; + + for (int iter = 0; iter < iterations; iter++) { + for (auto& w : interceptors) { + for (int projID : interceptableIDs) { + if (!w.HasIncomingProjectile(projID)) { + // Would normally add, but we skip to measure lookup cost + dummy++; + } + } + } + } + + auto t1 = std::chrono::high_resolution_clock::now(); + // Prevent optimization + if (dummy < 0) printf("never\n"); + return std::chrono::duration_cast(t1 - t0).count(); +} + +int main() { + // Scenario: 10 interceptor weapons, 200 interceptable projectiles, + // each weapon tracks 100 incoming projectiles (realistic for large battles) + const int W = 10; // interceptor weapons + const int P = 200; // interceptable projectiles in flight + const int I = 100; // incoming projectiles tracked per weapon + const int ITERS = 20; + + // --- correctness test --- + { + WeaponBefore wb; + WeaponAfter wa; + + for (int i = 0; i < 50; i++) { + wb.AddIncomingProjectile(i * 3); + wa.AddIncomingProjectile(i * 3); + } + + // Check membership + for (int i = 0; i < 50; i++) { + assert(wb.HasIncomingProjectile(i * 3) == true); + assert(wa.HasIncomingProjectile(i * 3) == true); + assert(wb.HasIncomingProjectile(i * 3 + 1) == false); + assert(wa.HasIncomingProjectile(i * 3 + 1) == false); + } + + // Check removal + wb.RemoveIncomingProjectile(15); + wa.RemoveIncomingProjectile(15); + assert(wb.HasIncomingProjectile(15) == false); + assert(wa.HasIncomingProjectile(15) == false); + + // Verify same membership after removal + for (int i = 0; i < 50; i++) { + if (i * 3 == 15) continue; + assert(wb.HasIncomingProjectile(i * 3) == true); + assert(wa.HasIncomingProjectile(i * 3) == true); + } + + printf("PASS correctness\n"); + } + + // --- performance test --- + { + std::vector interceptorsBefore(W); + std::vector interceptorsAfter(W); + + // Pre-populate each weapon with I tracked projectiles (IDs 0..I-1) + for (int w = 0; w < W; w++) { + for (int i = 0; i < I; i++) { + interceptorsBefore[w].AddIncomingProjectile(i); + interceptorsAfter[w].AddIncomingProjectile(i); + } + } + + // Interceptable projectiles: IDs from I to I+P-1 (none already tracked) + std::vector interceptableIDs(P); + for (int p = 0; p < P; p++) { + interceptableIDs[p] = I + p; + } + + // Warmup + simulateInterceptUpdate(interceptorsBefore, interceptableIDs, 2); + simulateInterceptUpdate(interceptorsAfter, interceptableIDs, 2); + + long long usBefore = simulateInterceptUpdate(interceptorsBefore, interceptableIDs, ITERS); + long long usAfter = simulateInterceptUpdate(interceptorsAfter, interceptableIDs, ITERS); + + double ratio = (double)usBefore / (double)usAfter; + + printf("BEFORE (vector std::find): %lld us\n", usBefore); + printf("AFTER (unordered_set::find): %lld us\n", usAfter); + printf("Ratio: %.1fx\n", ratio); + + // Expect significant speedup (typically 10x+ at these sizes) + assert(ratio > 2.0 && "Expected at least 2x speedup from vector->unordered_set"); + printf("PASS performance (%.1fx speedup)\n", ratio); + } + + printf("ALL TESTS PASSED\n"); + return 0; +} diff --git a/defects/spring-rts-0002/patch/spring-rts-0002.patch b/defects/spring-rts-0002/patch/spring-rts-0002.patch new file mode 100644 index 000000000..9f503bf15 --- /dev/null +++ b/defects/spring-rts-0002/patch/spring-rts-0002.patch @@ -0,0 +1,17 @@ +--- a/rts/Net/GameServer.cpp ++++ b/rts/Net/GameServer.cpp +@@ -2487,10 +2487,10 @@ + if (playerIter != players.end()) { + playerIter->SetValue("password", pwd); + +- LOG("[%s] changed password for client \"%s\" to \"%s\"", __func__, name.c_str(), pwd.c_str()); ++ LOG("[%s] changed password for client \"%s\"", __func__, name.c_str()); + } else { + AddAdditionalUser(name, pwd, false, spectator, team); + +- LOG("[%s] added %s \"%s\" with password \"%s\" to team %d", +- __func__, (spectator? "spectator": "player"), name.c_str(), pwd.c_str(), team ++ LOG("[%s] added %s \"%s\" to team %d", ++ __func__, (spectator? "spectator": "player"), name.c_str(), team + ); + } diff --git a/defects/spring-rts-0002/test/test_password_logging.py b/defects/spring-rts-0002/test/test_password_logging.py new file mode 100644 index 000000000..d64c96953 --- /dev/null +++ b/defects/spring-rts-0002/test/test_password_logging.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +""" +Unit test for spring-rts-0002 (CWE-312): GameServer logs passwords verbatim. + +Verifies that our patch removes password values from LOG() format strings +while preserving other diagnostic information (client name, team, role). +""" + +import re +import sys +import os + +PATCH_PATH = os.path.join(os.path.dirname(__file__), "..", "patch", "spring-rts-0002.patch") + + +def test_patch_removes_password_from_log(): + """Verify patch removes password from LOG() calls.""" + with open(PATCH_PATH) as f: + patch = f.read() + + # Lines removed (old code) should contain password in LOG + removed_lines = [l for l in patch.splitlines() if l.startswith("-") and not l.startswith("---")] + added_lines = [l for l in patch.splitlines() if l.startswith("+") and not l.startswith("+++")] + + # Old code logs pwd.c_str() + old_has_pwd = any("pwd.c_str()" in l for l in removed_lines) + assert old_has_pwd, "FAIL: expected old code to log pwd.c_str()" + + # New code must NOT log pwd.c_str() + new_has_pwd = any("pwd.c_str()" in l for l in added_lines) + assert not new_has_pwd, "FAIL: patched code still logs pwd.c_str()" + + # New code still logs name + new_has_name = any("name.c_str()" in l for l in added_lines) + assert new_has_name, "FAIL: patched code lost client name in log" + + print("PASS password_removed_from_log") + + +def test_patch_preserves_team_info(): + """Verify patch still logs team assignment info.""" + with open(PATCH_PATH) as f: + patch = f.read() + + added_lines = [l for l in patch.splitlines() if l.startswith("+") and not l.startswith("+++")] + + # Second LOG should still contain team %d + has_team = any("team" in l.lower() for l in added_lines) + assert has_team, "FAIL: patched code lost team info in log" + + print("PASS team_info_preserved") + + +def test_patch_format_string_consistent(): + """Verify format string argument counts match.""" + with open(PATCH_PATH) as f: + patch = f.read() + + added_lines = " ".join(l[1:] for l in patch.splitlines() if l.startswith("+") and not l.startswith("+++")) + + # Count %s and %d in added format strings + # First LOG: changed password for client "%s" = 2 args (__func__, name) + # Second LOG: added %s "%s" to team %d = 4 args (__func__, spectator/player, name, team) + # Just verify no pwd.c_str() appears + assert "pwd" not in added_lines, "FAIL: pwd still referenced in patched code" + + print("PASS format_string_consistent") + + +if __name__ == "__main__": + test_patch_removes_password_from_log() + test_patch_preserves_team_info() + test_patch_format_string_consistent() + print("ALL TESTS PASSED")