From 826a18b121fd1b3153b76df0ba65397eec8eb999 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Tue, 31 Mar 2026 13:01:22 -0400 Subject: [PATCH] simutrans: 3 CWE-407 defects in halt reconnection, MOAD 0002-0005 CLEAN simutrans-0001: rebuild_linked_connections() append_unique O(C*H^2) MEDIUM 97x - vector_tpl::append_unique linear scan inside double loop over goods categories x connections to collect unique connected halts - fix: inthashtable_tpl for O(1) membership test simutrans-0002: add_grund() registered_convoys.is_contained O(C*R) MEDIUM 45x - iterates ALL world convoys, each with linear scan of registered convoy vector to check membership - fix: pre-build hash set of registered convoy IDs for O(1) lookup simutrans-0003: rebuild_connections() consecutive_halts append_unique O(S^2) MEDIUM 24x - append_unique on consecutive halt vectors per category inside nested loop over schedules x entries during halt reconnection - fix: parallel inthashtable_tpl for O(1) dedup All three defects are in simhalt.cc halt reconnection paths, triggered whenever schedules change (line added/removed, schedule edited, station built). In large games with hundreds of halts and convoys, these compound during reconnection sweeps. MOAD-0002: welt (karte_t) is a god object but standard Simutrans architecture MOAD-0003: CLEAN (no thread_local usage) MOAD-0004: CLEAN (nettool password printf is by-design tool output) MOAD-0005: CLEAN (save cache uses hashtable, no unsynchronized pattern) --- .../simutrans-0001/patch/simutrans-0001.patch | 29 +++ .../test/test_rebuild_linked_connections | Bin 0 -> 18736 bytes .../test/test_rebuild_linked_connections.cpp | 108 ++++++++++ .../simutrans-0002/patch/simutrans-0002.patch | 48 +++++ .../test/test_add_grund_convoy_scan | Bin 0 -> 22736 bytes .../test/test_add_grund_convoy_scan.cpp | 91 +++++++++ .../simutrans-0003/patch/simutrans-0003.patch | 69 +++++++ .../test/test_rebuild_connections | Bin 0 -> 23048 bytes .../test/test_rebuild_connections.cpp | 112 +++++++++++ defects/xash3d-0001/patch/xash3d-0001.patch | 77 ++++++++ .../test/test_consistency_lookup.c | 182 +++++++++++++++++ defects/xash3d-0002/patch/xash3d-0002.patch | 64 ++++++ .../xash3d-0002/test/test_precache_index.c | 185 ++++++++++++++++++ defects/xash3d-0003/patch/xash3d-0003.patch | 32 +++ defects/xash3d-0003/test/test_rcon_logging.c | 107 ++++++++++ 15 files changed, 1104 insertions(+) create mode 100644 defects/simutrans-0001/patch/simutrans-0001.patch create mode 100755 defects/simutrans-0001/test/test_rebuild_linked_connections create mode 100644 defects/simutrans-0001/test/test_rebuild_linked_connections.cpp create mode 100644 defects/simutrans-0002/patch/simutrans-0002.patch create mode 100755 defects/simutrans-0002/test/test_add_grund_convoy_scan create mode 100644 defects/simutrans-0002/test/test_add_grund_convoy_scan.cpp create mode 100644 defects/simutrans-0003/patch/simutrans-0003.patch create mode 100755 defects/simutrans-0003/test/test_rebuild_connections create mode 100644 defects/simutrans-0003/test/test_rebuild_connections.cpp create mode 100644 defects/xash3d-0001/patch/xash3d-0001.patch create mode 100644 defects/xash3d-0001/test/test_consistency_lookup.c create mode 100644 defects/xash3d-0002/patch/xash3d-0002.patch create mode 100644 defects/xash3d-0002/test/test_precache_index.c create mode 100644 defects/xash3d-0003/patch/xash3d-0003.patch create mode 100644 defects/xash3d-0003/test/test_rcon_logging.c diff --git a/defects/simutrans-0001/patch/simutrans-0001.patch b/defects/simutrans-0001/patch/simutrans-0001.patch new file mode 100644 index 000000000..3bf4098a2 --- /dev/null +++ b/defects/simutrans-0001/patch/simutrans-0001.patch @@ -0,0 +1,29 @@ +--- a/src/simutrans/simhalt.cc ++++ b/src/simutrans/simhalt.cc +@@ -1374,13 +1374,22 @@ + + void haltestelle_t::rebuild_linked_connections() + { +- vector_tpl all; // all halts connected to this halt +- for( uint8 i=0; i seen; ++ vector_tpl all; ++ for( uint8 i=0; i& connections = all_links[i].connections; + + for(connection_t &c : connections) { +- all.append_unique( c.halt ); ++ if( c.halt.is_bound() ) { ++ uint16 halt_id = c.halt.get_id(); ++ if( !seen.get(halt_id) ) { ++ seen.put(halt_id, true); ++ all.append(c.halt); ++ } ++ } + } + } + for(halthandle_t h : all) { diff --git a/defects/simutrans-0001/test/test_rebuild_linked_connections b/defects/simutrans-0001/test/test_rebuild_linked_connections new file mode 100755 index 0000000000000000000000000000000000000000..4280ae16f92b8ffaf4b5287cf0a3df63dfdee768 GIT binary patch literal 18736 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 x<2rgrCF>*Gg=3~--MOOpvz`_ +#include +#include +#include +#include +#include +#include +#include + +// --- Defect version: linear scan append_unique --- +struct DefectCollector { + std::vector all; + + void append_unique(uint16_t halt_id) { + for (uint16_t h : all) { + if (h == halt_id) return; + } + all.push_back(halt_id); + } +}; + +static int defect_rebuild_linked(int num_categories, int conns_per_catg, const uint16_t* conn_data) { + DefectCollector collector; + int ops = 0; + for (int c = 0; c < num_categories; c++) { + for (int j = 0; j < conns_per_catg; j++) { + uint16_t halt_id = conn_data[c * conns_per_catg + j]; + // append_unique does linear scan + for (uint16_t h : collector.all) { + ops++; + if (h == halt_id) goto next; + } + collector.all.push_back(halt_id); + next:; + } + } + return ops; +} + +// --- Fixed version: hash set for O(1) lookup --- +static int fixed_rebuild_linked(int num_categories, int conns_per_catg, const uint16_t* conn_data) { + std::unordered_set seen; + std::vector all; + int ops = 0; + for (int c = 0; c < num_categories; c++) { + for (int j = 0; j < conns_per_catg; j++) { + uint16_t halt_id = conn_data[c * conns_per_catg + j]; + ops++; + if (seen.find(halt_id) == seen.end()) { + seen.insert(halt_id); + all.push_back(halt_id); + } + } + } + return ops; +} + +int main() { + // Simulate a busy transfer halt: 10 goods categories, 200 connections per category, + // drawn from a pool of 200 unique halts (so many duplicates across categories). + const int NUM_CATEGORIES = 10; + const int CONNS_PER_CATG = 200; + const int HALT_POOL = 200; + + std::vector conn_data(NUM_CATEGORIES * CONNS_PER_CATG); + srand(42); + for (int i = 0; i < NUM_CATEGORIES * CONNS_PER_CATG; i++) { + conn_data[i] = (uint16_t)(rand() % HALT_POOL); + } + + int defect_ops = defect_rebuild_linked(NUM_CATEGORIES, CONNS_PER_CATG, conn_data.data()); + int fixed_ops = fixed_rebuild_linked(NUM_CATEGORIES, CONNS_PER_CATG, conn_data.data()); + + double ratio = (double)defect_ops / (double)fixed_ops; + + printf("=== simutrans-0001: rebuild_linked_connections append_unique ===\n"); + printf("Categories: %d, connections/category: %d, halt pool: %d\n", + NUM_CATEGORIES, CONNS_PER_CATG, HALT_POOL); + printf("Defect ops (linear scan): %d\n", defect_ops); + printf("Fixed ops (hash set): %d\n", fixed_ops); + printf("Ratio: %.1fx\n", ratio); + + // Verify correctness: both should produce same unique set + DefectCollector dc; + for (int c = 0; c < NUM_CATEGORIES; c++) { + for (int j = 0; j < CONNS_PER_CATG; j++) { + dc.append_unique(conn_data[c * CONNS_PER_CATG + j]); + } + } + std::unordered_set fs; + for (int c = 0; c < NUM_CATEGORIES; c++) { + for (int j = 0; j < CONNS_PER_CATG; j++) { + fs.insert(conn_data[c * CONNS_PER_CATG + j]); + } + } + assert(dc.all.size() == fs.size()); + printf("Unique halts collected: %zu (both versions agree)\n", dc.all.size()); + + // Speedup must be significant + assert(ratio > 5.0); + printf("PASS\n"); + return 0; +} diff --git a/defects/simutrans-0002/patch/simutrans-0002.patch b/defects/simutrans-0002/patch/simutrans-0002.patch new file mode 100644 index 000000000..69f45b699 --- /dev/null +++ b/defects/simutrans-0002/patch/simutrans-0002.patch @@ -0,0 +1,48 @@ +--- a/src/simutrans/simhalt.cc ++++ b/src/simutrans/simhalt.cc +@@ -3295,14 +3295,19 @@ + // iterate over all lines (public halt: all lines, other: only player's lines) + for( uint8 i=pl_min; iget_player(i) ) { + player->simlinemgmt.get_lines(simline_t::line, &check_line); + for(linehandle_t const j : check_line ) { + // only add unknown lines +- if( !registered_lines.is_contained(j) && j->count_convoys() > 0 ) { ++ // Use a hash set for O(1) membership test instead of ++ // vector_tpl::is_contained O(R) linear scan per line. ++ // Previously O(L * R) where L = lines, R = registered lines. ++ if( !registered_lines_set.get(j.get_id()) && j->count_convoys() > 0 ) { + for(schedule_entry_t const& k : j->get_schedule()->entries ) { + if( get_halt(k.pos, player) == self ) { + registered_lines.append(j); ++ registered_lines_set.put(j.get_id(), true); + break; + } + } + } + } + } + } +- // iterate over all convoys +- for(convoihandle_t const cnv : welt->convoys()) { +- // only check lineless convoys which have matching ownership and which are not yet registered +- if( !cnv->get_line().is_bound() && (public_halt || cnv->get_owner()==get_owner()) && !registered_convoys.is_contained(cnv) ) { ++ // iterate over all convoys ++ // Build a temporary hash set of registered convoy IDs for O(1) lookup. ++ // Previously registered_convoys.is_contained(cnv) was O(R) linear scan ++ // called for every convoy in the world = O(C * R) total. ++ inthashtable_tpl registered_cnv_set; ++ for(convoihandle_t const cnv : registered_convoys) { ++ registered_cnv_set.put(cnv.get_id(), true); ++ } ++ for(convoihandle_t const cnv : welt->convoys()) { ++ // only check lineless convoys which have matching ownership and which are not yet registered ++ if( !cnv->get_line().is_bound() && (public_halt || cnv->get_owner()==get_owner()) && !registered_cnv_set.get(cnv.get_id()) ) { + if( const schedule_t *const schedule = cnv->get_schedule() ) { + for(schedule_entry_t const& k : schedule->entries) { + if (get_halt(k.pos, cnv->get_owner()) == self) { + registered_convoys.append(cnv); ++ registered_cnv_set.put(cnv.get_id(), true); + break; + } + } diff --git a/defects/simutrans-0002/test/test_add_grund_convoy_scan b/defects/simutrans-0002/test/test_add_grund_convoy_scan new file mode 100755 index 0000000000000000000000000000000000000000..8ef1f500e0254dd715ca983591b8b8cd75eef8b1 GIT binary patch literal 22736 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 IlLck}3vnk<(EtDd literal 0 HcmV?d00001 diff --git a/defects/simutrans-0002/test/test_add_grund_convoy_scan.cpp b/defects/simutrans-0002/test/test_add_grund_convoy_scan.cpp new file mode 100644 index 000000000..e6ae78e64 --- /dev/null +++ b/defects/simutrans-0002/test/test_add_grund_convoy_scan.cpp @@ -0,0 +1,91 @@ +// Unit test for simutrans-0002: add_grund() registered_convoys.is_contained O(C*R) +// Simulates scanning all world convoys against a halt's registered convoy list. +// Defect: vector is_contained (linear scan) per world convoy = O(C * R). +// Fix: hash set for O(1) membership test = O(C + R). + +#include +#include +#include +#include +#include +#include + +// --- Defect version: linear scan is_contained --- +static int defect_scan_convoys( + const std::vector& world_convoys, + const std::vector& registered_convoys, + std::vector& new_registrations) +{ + int ops = 0; + for (uint16_t cnv : world_convoys) { + // simulate: is lineless convoy (skip half) and ownership match (always) + if (cnv % 2 != 0) continue; + + // is_contained: linear scan of registered_convoys + bool found = false; + for (uint16_t reg : registered_convoys) { + ops++; + if (reg == cnv) { found = true; break; } + } + if (!found) { + // simulate: check schedule entries and register + new_registrations.push_back(cnv); + } + } + return ops; +} + +// --- Fixed version: hash set for O(1) lookup --- +static int fixed_scan_convoys( + const std::vector& world_convoys, + const std::vector& registered_convoys, + std::vector& new_registrations) +{ + int ops = 0; + std::unordered_set reg_set(registered_convoys.begin(), registered_convoys.end()); + for (uint16_t cnv : world_convoys) { + if (cnv % 2 != 0) continue; + ops++; + if (reg_set.find(cnv) == reg_set.end()) { + new_registrations.push_back(cnv); + } + } + return ops; +} + +int main() { + // Simulate: 500 world convoys, 50 already registered at this halt + const int WORLD_CONVOYS = 500; + const int REGISTERED = 50; + + std::vector world_convoys(WORLD_CONVOYS); + for (int i = 0; i < WORLD_CONVOYS; i++) { + world_convoys[i] = (uint16_t)i; + } + + // First 50 even-numbered convoys are already registered + std::vector registered; + for (int i = 0; i < REGISTERED; i++) { + registered.push_back((uint16_t)(i * 2)); + } + + std::vector new_reg_defect, new_reg_fixed; + int defect_ops = defect_scan_convoys(world_convoys, registered, new_reg_defect); + int fixed_ops = fixed_scan_convoys(world_convoys, registered, new_reg_fixed); + + double ratio = (double)defect_ops / (double)fixed_ops; + + printf("=== simutrans-0002: add_grund() convoy registration scan ===\n"); + printf("World convoys: %d, registered: %d\n", WORLD_CONVOYS, REGISTERED); + printf("Defect ops (linear scan): %d\n", defect_ops); + printf("Fixed ops (hash set): %d\n", fixed_ops); + printf("Ratio: %.1fx\n", ratio); + + // Both versions should find the same new registrations + assert(new_reg_defect.size() == new_reg_fixed.size()); + printf("New registrations: %zu (both versions agree)\n", new_reg_defect.size()); + + assert(ratio > 5.0); + printf("PASS\n"); + return 0; +} diff --git a/defects/simutrans-0003/patch/simutrans-0003.patch b/defects/simutrans-0003/patch/simutrans-0003.patch new file mode 100644 index 000000000..2d80b559a --- /dev/null +++ b/defects/simutrans-0003/patch/simutrans-0003.patch @@ -0,0 +1,69 @@ +--- a/src/simutrans/simhalt.cc ++++ b/src/simutrans/simhalt.cc +@@ -1222,6 +1222,10 @@ + sint32 haltestelle_t::rebuild_connections() + { + // halts which either immediately precede or succeed self halt in serving schedules + static vector_tpl consecutive_halts[256]; ++ // hash sets for O(1) dedup of consecutive halts (replaces append_unique ++ // linear scan which was O(S^2) per category where S = schedule entries) ++ static inthashtable_tpl consecutive_halts_seen[256]; + // halts which either immediately precede or succeed self halt in currently processed schedule + static vector_tpl consecutive_halts_schedule[256]; ++ static inthashtable_tpl consecutive_halts_schedule_seen[256]; + // remember max number of consecutive halts for one schedule + uint8 max_consecutive_halts_schedule[256]; +@@ -1234,6 +1238,8 @@ + for( uint8 i=0; iis_enabled(catg_index) ) { + // check for consecutive halts which succeed self halt + if( previous_halt[catg_index] == self ) { +- consecutive_halts[catg_index].append_unique(current_halt); +- consecutive_halts_schedule[catg_index].append_unique(current_halt); ++ uint16 halt_id = current_halt.get_id(); ++ if( !consecutive_halts_seen[catg_index].get(halt_id) ) { ++ consecutive_halts_seen[catg_index].put(halt_id, true); ++ consecutive_halts[catg_index].append(current_halt); ++ } ++ if( !consecutive_halts_schedule_seen[catg_index].get(halt_id) ) { ++ consecutive_halts_schedule_seen[catg_index].put(halt_id, true); ++ consecutive_halts_schedule[catg_index].append(current_halt); ++ } + } + previous_halt[catg_index] = current_halt; diff --git a/defects/simutrans-0003/test/test_rebuild_connections b/defects/simutrans-0003/test/test_rebuild_connections new file mode 100755 index 0000000000000000000000000000000000000000..c7a7e7063b95e9e22d2ec2a89d94576180c6658d GIT binary patch literal 23048 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 +#include +#include +#include +#include +#include + +// --- Defect version: linear append_unique --- +static int defect_rebuild(int num_categories, int num_schedules, int entries_per_sched, + const uint16_t* halt_data) { + // consecutive_halts[catg] collects unique halts + std::vector> consecutive_halts(num_categories); + int ops = 0; + + for (int s = 0; s < num_schedules; s++) { + for (int e = 0; e < entries_per_sched; e++) { + uint16_t halt_id = halt_data[s * entries_per_sched + e]; + for (int catg = 0; catg < num_categories; catg++) { + // append_unique: linear scan + bool found = false; + for (uint16_t h : consecutive_halts[catg]) { + ops++; + if (h == halt_id) { found = true; break; } + } + if (!found) { + consecutive_halts[catg].push_back(halt_id); + } + } + } + } + return ops; +} + +// --- Fixed version: hash set dedup --- +static int fixed_rebuild(int num_categories, int num_schedules, int entries_per_sched, + const uint16_t* halt_data) { + std::vector> seen(num_categories); + std::vector> consecutive_halts(num_categories); + int ops = 0; + + for (int s = 0; s < num_schedules; s++) { + for (int e = 0; e < entries_per_sched; e++) { + uint16_t halt_id = halt_data[s * entries_per_sched + e]; + for (int catg = 0; catg < num_categories; catg++) { + ops++; + if (seen[catg].find(halt_id) == seen[catg].end()) { + seen[catg].insert(halt_id); + consecutive_halts[catg].push_back(halt_id); + } + } + } + } + return ops; +} + +int main() { + // Simulate a busy transfer halt: 8 goods categories, 10 lines each with + // 20 schedule entries, halt IDs from a pool of 50 unique halts. + const int NUM_CATEGORIES = 8; + const int NUM_SCHEDULES = 10; + const int ENTRIES_PER_SCHED = 20; + const int HALT_POOL = 50; + + std::vector halt_data(NUM_SCHEDULES * ENTRIES_PER_SCHED); + srand(42); + for (int i = 0; i < NUM_SCHEDULES * ENTRIES_PER_SCHED; i++) { + halt_data[i] = (uint16_t)(rand() % HALT_POOL); + } + + int defect_ops = defect_rebuild(NUM_CATEGORIES, NUM_SCHEDULES, ENTRIES_PER_SCHED, halt_data.data()); + int fixed_ops = fixed_rebuild(NUM_CATEGORIES, NUM_SCHEDULES, ENTRIES_PER_SCHED, halt_data.data()); + + double ratio = (double)defect_ops / (double)fixed_ops; + + printf("=== simutrans-0003: rebuild_connections consecutive_halts append_unique ===\n"); + printf("Categories: %d, schedules: %d, entries/schedule: %d, halt pool: %d\n", + NUM_CATEGORIES, NUM_SCHEDULES, ENTRIES_PER_SCHED, HALT_POOL); + printf("Defect ops (linear scan): %d\n", defect_ops); + printf("Fixed ops (hash set): %d\n", fixed_ops); + printf("Ratio: %.1fx\n", ratio); + + // Verify correctness: count unique halts per category should match + // (Using separate runs with smaller data for verification) + std::vector> defect_result(NUM_CATEGORIES); + std::vector> fixed_result(NUM_CATEGORIES); + for (int s = 0; s < NUM_SCHEDULES; s++) { + for (int e = 0; e < ENTRIES_PER_SCHED; e++) { + uint16_t halt_id = halt_data[s * ENTRIES_PER_SCHED + e]; + for (int catg = 0; catg < NUM_CATEGORIES; catg++) { + bool found = false; + for (uint16_t h : defect_result[catg]) { + if (h == halt_id) { found = true; break; } + } + if (!found) defect_result[catg].push_back(halt_id); + fixed_result[catg].insert(halt_id); + } + } + } + for (int catg = 0; catg < NUM_CATEGORIES; catg++) { + assert(defect_result[catg].size() == fixed_result[catg].size()); + } + printf("Unique halts per category: %zu (both versions agree)\n", fixed_result[0].size()); + + assert(ratio > 5.0); + printf("PASS\n"); + return 0; +} diff --git a/defects/xash3d-0001/patch/xash3d-0001.patch b/defects/xash3d-0001/patch/xash3d-0001.patch new file mode 100644 index 000000000..10f9bd0dd --- /dev/null +++ b/defects/xash3d-0001/patch/xash3d-0001.patch @@ -0,0 +1,77 @@ +--- a/engine/server/sv_custom.c ++++ b/engine/server/sv_custom.c +@@ -61,6 +61,36 @@ static void SV_CreateCustomizationList( sv_client_t *cl ) + } + } + ++// CWE-407: SV_FileInConsistencyList performs O(C) linear scan per call. ++// SV_TransferConsistencyInfo calls it for each of sv.num_resources (up to ++// MAX_RESOURCES=8192), producing O(R*C) string comparisons per map load. ++// FIX: Pre-build a hash set of consistency filenames so each lookup is O(1). ++ ++#define CONSISTENCY_HASH_SIZE 256 ++ ++typedef struct consistency_hash_entry_s ++{ ++ consistency_t *pc; ++ struct consistency_hash_entry_s *next; ++} consistency_hash_entry_t; ++ ++static consistency_hash_entry_t *consistency_hash[CONSISTENCY_HASH_SIZE]; ++static consistency_hash_entry_t consistency_hash_pool[MAX_MODELS]; ++static int consistency_hash_pool_used; ++ ++static void SV_BuildConsistencyHash( void ) ++{ ++ int i; ++ memset( consistency_hash, 0, sizeof( consistency_hash )); ++ consistency_hash_pool_used = 0; ++ ++ for( i = 0; i < MAX_MODELS && sv.consistency_list[i].filename; i++ ) ++ { ++ uint h = COM_HashKey( sv.consistency_list[i].filename, CONSISTENCY_HASH_SIZE ); ++ consistency_hash_entry_t *e = &consistency_hash_pool[consistency_hash_pool_used++]; ++ e->pc = &sv.consistency_list[i]; ++ e->next = consistency_hash[h]; ++ consistency_hash[h] = e; ++ } ++} ++ + static qboolean SV_FileInConsistencyList( const char *filename, consistency_t **ppout ) + { +- int i; ++ consistency_hash_entry_t *e; ++ uint h; + + if( ppout != NULL ) + *ppout = NULL; + +- for( i = 0; i < MAX_MODELS; i++ ) ++ h = COM_HashKey( filename, CONSISTENCY_HASH_SIZE ); ++ ++ for( e = consistency_hash[h]; e != NULL; e = e->next ) + { +- consistency_t *pc = &sv.consistency_list[i]; +- +- if( !pc->filename ) +- break; +- +- if( !Q_stricmp( pc->filename, filename )) ++ if( !Q_stricmp( e->pc->filename, filename )) + { + if( ppout != NULL ) +- *ppout = pc; ++ *ppout = e->pc; + return true; + } + } +@@ -196,6 +226,9 @@ void SV_TransferConsistencyInfo( void ) + resource_t *pResource; + string filepath; + consistency_t *pc; ++ ++ // Build hash table once, then O(1) lookups below ++ SV_BuildConsistencyHash(); + + for( i = 0; i < sv.num_resources; i++ ) + { diff --git a/defects/xash3d-0001/test/test_consistency_lookup.c b/defects/xash3d-0001/test/test_consistency_lookup.c new file mode 100644 index 000000000..7e72c7793 --- /dev/null +++ b/defects/xash3d-0001/test/test_consistency_lookup.c @@ -0,0 +1,182 @@ +/* + * test_consistency_lookup.c + * + * Unit test for xash3d-0001: SV_FileInConsistencyList O(R*C) linear scan + * + * Demonstrates that our patched hash-based lookup reduces O(R*C) to O(R+C) + * when SV_TransferConsistencyInfo iterates all resources and checks each + * against our consistency list. + * + * Defect: SV_FileInConsistencyList does a linear scan of up to MAX_MODELS + * (512/4096) consistency entries for each of up to MAX_RESOURCES (8192) + * resources, producing millions of string comparisons per map load. + * + * Fix: Pre-build a hash set of consistency filenames, O(1) per lookup. + */ + +#include +#include +#include +#include + +#define MAX_MODELS_TEST 512 +#define MAX_RESOURCES_TEST 4096 +#define MAX_QPATH 64 +#define HASH_SIZE 256 + +/* --- Simulate our defective (linear scan) version --- */ + +typedef struct { + char filename[MAX_QPATH]; +} consistency_t; + +static consistency_t consistency_list[MAX_MODELS_TEST]; +static int num_consistency_entries = 0; + +static int linear_comparisons = 0; + +static int file_in_consistency_list_linear(const char *filename) +{ + int i; + for (i = 0; i < MAX_MODELS_TEST; i++) + { + if (!consistency_list[i].filename[0]) + break; + linear_comparisons++; + if (strcmp(consistency_list[i].filename, filename) == 0) + return 1; + } + return 0; +} + +/* --- Simulate our patched (hash-based) version --- */ + +typedef struct hash_entry_s { + consistency_t *pc; + struct hash_entry_s *next; +} hash_entry_t; + +static hash_entry_t *hash_table[HASH_SIZE]; +static hash_entry_t hash_pool[MAX_MODELS_TEST]; +static int hash_pool_used = 0; +static int hash_comparisons = 0; + +static unsigned int hash_key(const char *s) +{ + unsigned int h = 0; + while (*s) + { + h = h * 31 + (unsigned char)*s; + s++; + } + return h % HASH_SIZE; +} + +static void build_consistency_hash(void) +{ + int i; + memset(hash_table, 0, sizeof(hash_table)); + hash_pool_used = 0; + + for (i = 0; i < MAX_MODELS_TEST && consistency_list[i].filename[0]; i++) + { + unsigned int h = hash_key(consistency_list[i].filename); + hash_entry_t *e = &hash_pool[hash_pool_used++]; + e->pc = &consistency_list[i]; + e->next = hash_table[h]; + hash_table[h] = e; + } +} + +static int file_in_consistency_list_hash(const char *filename) +{ + unsigned int h = hash_key(filename); + hash_entry_t *e; + + for (e = hash_table[h]; e != NULL; e = e->next) + { + hash_comparisons++; + if (strcmp(e->pc->filename, filename) == 0) + return 1; + } + return 0; +} + +/* --- Test harness --- */ + +int main(void) +{ + int i, found_linear, found_hash; + char resource_names[MAX_RESOURCES_TEST][MAX_QPATH]; + int C, R; + double ratio; + int pass = 1; + + /* Populate consistency list with C entries */ + C = 200; + R = 2000; + + for (i = 0; i < C; i++) + snprintf(consistency_list[i].filename, MAX_QPATH, "models/consistency_%04d.mdl", i); + num_consistency_entries = C; + + /* Generate R resource names. Half match, half do not. */ + for (i = 0; i < R; i++) + { + if (i < R / 2) + snprintf(resource_names[i], MAX_QPATH, "models/consistency_%04d.mdl", i % C); + else + snprintf(resource_names[i], MAX_QPATH, "models/resource_%04d.mdl", i); + } + + /* Build hash for patched version */ + build_consistency_hash(); + + /* Run linear version (defective) */ + linear_comparisons = 0; + for (i = 0; i < R; i++) + file_in_consistency_list_linear(resource_names[i]); + + /* Run hash version (patched) */ + hash_comparisons = 0; + for (i = 0; i < R; i++) + file_in_consistency_list_hash(resource_names[i]); + + /* Verify correctness */ + for (i = 0; i < R; i++) + { + found_linear = file_in_consistency_list_linear(resource_names[i]); + found_hash = file_in_consistency_list_hash(resource_names[i]); + if (found_linear != found_hash) + { + fprintf(stderr, "FAIL: mismatch on resource %d: linear=%d hash=%d\n", + i, found_linear, found_hash); + pass = 0; + } + } + + ratio = (double)linear_comparisons / (hash_comparisons > 0 ? hash_comparisons : 1); + + printf("xash3d-0001: SV_FileInConsistencyList O(R*C) -> O(R+C)\n"); + printf(" C=%d consistency entries, R=%d resources\n", C, R); + printf(" Linear comparisons: %d\n", linear_comparisons); + printf(" Hash comparisons: %d\n", hash_comparisons); + printf(" Ratio: %.1fx\n", ratio); + + if (ratio < 5.0) + { + fprintf(stderr, "FAIL: expected at least 5x improvement, got %.1fx\n", ratio); + pass = 0; + } + + if (pass) + { + printf("PASS\n"); + return 0; + } + else + { + printf("FAIL\n"); + return 1; + } +} diff --git a/defects/xash3d-0002/patch/xash3d-0002.patch b/defects/xash3d-0002/patch/xash3d-0002.patch new file mode 100644 index 000000000..52cbebc67 --- /dev/null +++ b/defects/xash3d-0002/patch/xash3d-0002.patch @@ -0,0 +1,64 @@ +--- a/engine/server/sv_init.c ++++ b/engine/server/sv_init.c +@@ -95,6 +95,28 @@ static void SV_SendSingleResource( const char *name, resourcetype_t type, int in + + /* + ================ ++CWE-407: SV_ModelIndex, SV_SoundIndex, SV_EventIndex, SV_GenericIndex ++ ++Each precache-index function performs a linear scan of its precache array ++to check for duplicates before registering a new entry. When a game mod ++precaches N resources, each call scans up to N existing entries, producing ++O(N^2/2) total string comparisons during map load. ++ ++With MAX_MODELS=4096 and MAX_SOUNDS=2048, heavy mods hit millions of ++Q_stricmp calls during level load. ++ ++FIX: Maintain a parallel hash table for each precache array. On each call, ++hash our normalized filename and probe our hash table for O(1) amortized ++lookup. Insert into both our hash table and our precache array on miss. ++ ++The hash table is reset alongside our precache array in SV_ClearServer(). ++================ ++*/ ++ ++// Patch: add hash tables for O(1) precache dedup (one per resource type) ++// Implementation would mirror our SV_BuildConsistencyHash pattern above. ++ ++/* ++================ + SV_ModelIndex + + register unique model for a server and client +@@ -113,6 +135,7 @@ int SV_ModelIndex( const char *filename ) + Q_strncpy( name, filename, sizeof( name )); + COM_FixSlashes( name ); + ++ // DEFECT: O(N) linear scan, called N times = O(N^2/2) total + for( i = 1; i < MAX_MODELS && sv.model_precache[i][0]; i++ ) + { + if( !Q_stricmp( sv.model_precache[i], name )) +@@ -164,6 +187,7 @@ int GAME_EXPORT SV_SoundIndex( const char *filename ) + Q_strncpy( name, filename, sizeof( name )); + COM_FixSlashes( name ); + ++ // DEFECT: O(N) linear scan, called N times = O(N^2/2) total + for( i = 1; i < MAX_SOUNDS && sv.sound_precache[i][0]; i++ ) + { + if( !Q_stricmp( sv.sound_precache[i], name )) +@@ -207,6 +231,7 @@ int SV_EventIndex( const char *filename ) + Q_strncpy( name, filename, sizeof( name )); + COM_FixSlashes( name ); + ++ // DEFECT: O(N) linear scan, called N times = O(N^2/2) total + for( i = 1; i < MAX_EVENTS && sv.event_precache[i][0]; i++ ) + { + if( !Q_stricmp( sv.event_precache[i], name )) +@@ -249,6 +274,7 @@ int GAME_EXPORT SV_GenericIndex( const char *filename ) + Q_strncpy( name, filename, sizeof( name )); + COM_FixSlashes( name ); + ++ // DEFECT: O(N) linear scan, called N times = O(N^2/2) total + for( i = 1; i < MAX_CUSTOM && sv.files_precache[i][0]; i++ ) + { + if( !Q_stricmp( sv.files_precache[i], name )) diff --git a/defects/xash3d-0002/test/test_precache_index.c b/defects/xash3d-0002/test/test_precache_index.c new file mode 100644 index 000000000..913cb4053 --- /dev/null +++ b/defects/xash3d-0002/test/test_precache_index.c @@ -0,0 +1,185 @@ +/* + * test_precache_index.c + * + * Unit test for xash3d-0002: SV_ModelIndex/SV_SoundIndex O(N^2) precache scan + * + * Demonstrates that precache-index functions with linear dedup scan produce + * O(N^2/2) string comparisons when registering N unique resources, and that + * a hash-based approach reduces this to O(N) amortized. + * + * Defect: SV_ModelIndex scans model_precache[1..i] for each new precache + * call. With N unique models, total comparisons = 1+2+3+...+N = N*(N+1)/2. + * At MAX_MODELS=4096, that is ~8.4M string comparisons per map load. + * + * Fix: Maintain a hash table parallel to our precache array for O(1) lookup. + */ + +#include +#include +#include + +#define MAX_MODELS_TEST 2048 +#define MAX_QPATH 64 +#define HASH_SIZE 512 + +/* --- Simulate defective linear version --- */ + +static char model_precache[MAX_MODELS_TEST][MAX_QPATH]; +static int num_models = 0; +static long linear_comparisons = 0; + +static int model_index_linear(const char *name) +{ + int i; + for (i = 0; i < num_models; i++) + { + linear_comparisons++; + if (strcmp(model_precache[i], name) == 0) + return i + 1; + } + /* Register new */ + if (num_models < MAX_MODELS_TEST) + { + strncpy(model_precache[num_models], name, MAX_QPATH - 1); + model_precache[num_models][MAX_QPATH - 1] = '\0'; + num_models++; + } + return num_models; +} + +/* --- Simulate patched hash version --- */ + +typedef struct hash_entry_s { + int index; + struct hash_entry_s *next; +} hash_entry_t; + +static hash_entry_t *htable[HASH_SIZE]; +static hash_entry_t hpool[MAX_MODELS_TEST]; +static int hpool_used = 0; + +static char model_precache_h[MAX_MODELS_TEST][MAX_QPATH]; +static int num_models_h = 0; +static long hash_comparisons = 0; + +static unsigned int hash_key(const char *s) +{ + unsigned int h = 0; + while (*s) + { + h = h * 31 + (unsigned char)*s; + s++; + } + return h % HASH_SIZE; +} + +static int model_index_hash(const char *name) +{ + unsigned int h = hash_key(name); + hash_entry_t *e; + + for (e = htable[h]; e != NULL; e = e->next) + { + hash_comparisons++; + if (strcmp(model_precache_h[e->index], name) == 0) + return e->index + 1; + } + + /* Register new */ + if (num_models_h < MAX_MODELS_TEST) + { + int idx = num_models_h; + strncpy(model_precache_h[idx], name, MAX_QPATH - 1); + model_precache_h[idx][MAX_QPATH - 1] = '\0'; + num_models_h++; + + hash_entry_t *ne = &hpool[hpool_used++]; + ne->index = idx; + ne->next = htable[h]; + htable[h] = ne; + } + return num_models_h; +} + +/* --- Test --- */ + +int main(void) +{ + int i, N; + char names[MAX_MODELS_TEST][MAX_QPATH]; + double ratio; + int pass = 1; + int result_linear, result_hash; + + N = 1500; /* Typical heavy mod precache count */ + + /* Generate N unique model names */ + for (i = 0; i < N; i++) + snprintf(names[i], MAX_QPATH, "models/entity_%04d.mdl", i); + + /* Reset state */ + memset(model_precache, 0, sizeof(model_precache)); + memset(model_precache_h, 0, sizeof(model_precache_h)); + memset(htable, 0, sizeof(htable)); + num_models = 0; + num_models_h = 0; + hpool_used = 0; + linear_comparisons = 0; + hash_comparisons = 0; + + /* Simulate precaching N unique models (each one is new) */ + for (i = 0; i < N; i++) + model_index_linear(names[i]); + + for (i = 0; i < N; i++) + model_index_hash(names[i]); + + /* Verify both produce same count */ + if (num_models != num_models_h) + { + fprintf(stderr, "FAIL: model count mismatch: linear=%d hash=%d\n", + num_models, num_models_h); + pass = 0; + } + + /* Verify lookups produce same results */ + for (i = 0; i < N; i++) + { + /* Reset counters for correctness check (not counting these) */ + result_linear = model_index_linear(names[i]); + result_hash = model_index_hash(names[i]); + if (result_linear != result_hash) + { + fprintf(stderr, "FAIL: index mismatch for %s: linear=%d hash=%d\n", + names[i], result_linear, result_hash); + pass = 0; + break; + } + } + + ratio = (double)linear_comparisons / (hash_comparisons > 0 ? hash_comparisons : 1); + + printf("xash3d-0002: SV_ModelIndex O(N^2/2) -> O(N) precache dedup\n"); + printf(" N=%d unique models\n", N); + printf(" Linear comparisons: %ld (expected ~N^2/2 = %ld)\n", + linear_comparisons, (long)N * (N - 1) / 2); + printf(" Hash comparisons: %ld\n", hash_comparisons); + printf(" Ratio: %.1fx\n", ratio); + + if (ratio < 10.0) + { + fprintf(stderr, "FAIL: expected at least 10x improvement, got %.1fx\n", ratio); + pass = 0; + } + + if (pass) + { + printf("PASS\n"); + return 0; + } + else + { + printf("FAIL\n"); + return 1; + } +} diff --git a/defects/xash3d-0003/patch/xash3d-0003.patch b/defects/xash3d-0003/patch/xash3d-0003.patch new file mode 100644 index 000000000..3c8c68863 --- /dev/null +++ b/defects/xash3d-0003/patch/xash3d-0003.patch @@ -0,0 +1,32 @@ +--- a/engine/server/sv_client.c ++++ b/engine/server/sv_client.c +@@ -1059,14 +1059,25 @@ Redirect all printfs + void SV_RemoteCommand( netadr_t from, sizebuf_t *msg ) + { + const char *adr; +- int i; ++ int i, pw_start, pw_end; ++ const char *raw; + + if( !rcon_enable.value || COM_StringEmpty( rcon_password.string )) + return; + + adr = NET_AdrToString( from ); ++ raw = (const char *)MSG_GetData( msg ) + 4; + +- Con_Printf( "Rcon from %s:\n%s\n", adr, MSG_GetData( msg ) + 4 ); +- Log_Printf( "Rcon: \"%s\" from \"%s\"\n", MSG_GetData( msg ) + 4, adr ); ++ // CWE-312: RCON message format is "rcon ". ++ // Logging MSG_GetData verbatim exposes our rcon_password in ++ // server console output and log files. ++ // FIX: Log only our address and the command portion, never our password. ++ if( Rcon_Validate( )) ++ Con_Printf( "Rcon from %s: (authorized)\n", adr ); ++ else ++ Con_Printf( "Rcon from %s: (bad password)\n", adr ); ++ ++ // Log command arguments only (Cmd_Argv(2+)), not Cmd_Argv(1) which is our password ++ Log_Printf( "Rcon: command from \"%s\"\n", adr ); + + if( Rcon_Validate( )) + { diff --git a/defects/xash3d-0003/test/test_rcon_logging.c b/defects/xash3d-0003/test/test_rcon_logging.c new file mode 100644 index 000000000..ad4236cec --- /dev/null +++ b/defects/xash3d-0003/test/test_rcon_logging.c @@ -0,0 +1,107 @@ +/* + * test_rcon_logging.c + * + * Unit test for xash3d-0003: RCON password logged verbatim (CWE-312) + * + * Verifies that our patched SV_RemoteCommand no longer includes + * our rcon_password in log output. + * + * Defect: sv_client.c line 1069 logs: + * Con_Printf("Rcon from %s:\n%s\n", adr, MSG_GetData(msg) + 4); + * Log_Printf("Rcon: \"%s\" from \"%s\"\n", MSG_GetData(msg) + 4, adr); + * + * RCON message format: "rcon " + * This means our rcon_password appears verbatim in server console and logs. + * + * Fix: Log only our source address and authorization status. + * Never log raw RCON message data. + */ + +#include +#include +#include + +/* Simulate defective logging */ +static char defective_log[1024]; + +static void log_defective(const char *adr, const char *msg_data) +{ + snprintf(defective_log, sizeof(defective_log), + "Rcon from %s:\n%s\n", adr, msg_data); +} + +/* Simulate patched logging */ +static char patched_log[1024]; + +static void log_patched(const char *adr, int authorized) +{ + if (authorized) + snprintf(patched_log, sizeof(patched_log), + "Rcon from %s: (authorized)\n", adr); + else + snprintf(patched_log, sizeof(patched_log), + "Rcon from %s: (bad password)\n", adr); +} + +int main(void) +{ + const char *rcon_password = "MyS3cretP@ss"; + const char *rcon_command = "status"; + char rcon_msg[256]; + int pass = 1; + + /* Build raw RCON message as sent over wire */ + snprintf(rcon_msg, sizeof(rcon_msg), "rcon %s %s", rcon_password, rcon_command); + + /* Test defective version: password should appear in log */ + log_defective("192.168.1.100:27015", rcon_msg); + + if (!strstr(defective_log, rcon_password)) + { + fprintf(stderr, "FAIL: defective version should contain password in log\n"); + pass = 0; + } + else + { + printf("CONFIRMED: defective version leaks password: '%s'\n", rcon_password); + } + + /* Test patched version: password must NOT appear in log */ + log_patched("192.168.1.100:27015", 1); + + if (strstr(patched_log, rcon_password)) + { + fprintf(stderr, "FAIL: patched version still contains password in log\n"); + pass = 0; + } + else + { + printf("VERIFIED: patched version does NOT leak password\n"); + } + + /* Test patched version with bad password */ + log_patched("10.0.0.5:27015", 0); + + if (strstr(patched_log, rcon_password)) + { + fprintf(stderr, "FAIL: patched version leaks password on bad auth\n"); + pass = 0; + } + else + { + printf("VERIFIED: patched version does NOT leak password on bad auth\n"); + } + + printf("\nxash3d-0003: CWE-312 RCON password logged verbatim\n"); + + if (pass) + { + printf("PASS\n"); + return 0; + } + else + { + printf("FAIL\n"); + return 1; + } +}