diff --git a/defects/freeorion-0001/patch/freeorion-0001.patch b/defects/freeorion-0001/patch/freeorion-0001.patch new file mode 100644 index 000000000..3fc793b0c --- /dev/null +++ b/defects/freeorion-0001/patch/freeorion-0001.patch @@ -0,0 +1,56 @@ +--- a/Empire/Empire.cpp ++++ b/Empire/Empire.cpp +@@ -2263,6 +2263,14 @@ std::vector Empire::CheckResearchProgress( + const std::vector>& costs_times) + { + SanitizeResearchQueue(m_research_queue); ++ ++ // Build hash map for O(1) lookup of tech cost/time by name. ++ // Without this, each lookup is a linear scan of costs_times, making ++ // both loops below O(Q*T) and O(T*T) respectively. ++ boost::unordered_flat_map> costs_times_map; ++ costs_times_map.reserve(costs_times.size()); ++ for (const auto& [name, cost, time] : costs_times) ++ costs_times_map.emplace(name, std::pair{cost, time}); + + float spent_rp = 0.0f; + const float total_rp_available = m_research_pool.TotalAvailable(); +@@ -2274,15 +2282,13 @@ std::vector Empire::CheckResearchProgress( + // process items on queue + std::vector to_erase_from_queue_and_grant_next_turn; + for (auto& elem : m_research_queue | range_filter(has_allocated_rp)) { +- const auto is_tech = [tech_name{elem.name}](const std::tuple& ct) +- { return std::get<0>(ct) == tech_name; }; +- +- const auto ct_it = range_find_if(costs_times, is_tech); +- if (ct_it == costs_times.end()) { ++ const auto ct_it = costs_times_map.find(elem.name); ++ if (ct_it == costs_times_map.end()) { + ErrorLogger() << "Missing tech " << elem.name << " cost time in CheckResearchProgress!"; + continue; + } +- const float tech_cost = static_cast(std::get<1>(*ct_it)); ++ const float tech_cost = static_cast(ct_it->second.first); + + float& progress = m_research_progress[elem.name]; + progress += elem.allocated_rp / std::max(EPSILON, tech_cost); +@@ -2328,16 +2334,13 @@ std::vector Empire::CheckResearchProgress( + if (progress_fraction >= 1.0) + continue; + +- const auto is_tech = [tech_name{tech_name}](const std::tuple& ct) +- { return std::get<0>(ct) == tech_name; }; +- +- const auto ct_it = range_find_if(costs_times, is_tech); +- if (ct_it == costs_times.end()) { ++ const auto ct_it = costs_times_map.find(tech_name); ++ if (ct_it == costs_times_map.end()) { + ErrorLogger() << "Missing tech " << tech_name << " cost time in CheckResearchProgress!"; + continue; + } + +- const double remaining_cost = std::get<1>(*ct_it) * (1.0 - progress_fraction); ++ const double remaining_cost = ct_it->second.first * (1.0 - progress_fraction); + costs_to_complete_available_unpaused_techs.emplace_back(remaining_cost, tech_name); + } + std::stable_sort(costs_to_complete_available_unpaused_techs.begin(), diff --git a/defects/freeorion-0001/test/test b/defects/freeorion-0001/test/test new file mode 100755 index 000000000..0b16a0281 Binary files /dev/null and b/defects/freeorion-0001/test/test differ diff --git a/defects/freeorion-0001/test/test_freeorion_0001.cpp b/defects/freeorion-0001/test/test_freeorion_0001.cpp new file mode 100644 index 000000000..1c0bb7110 --- /dev/null +++ b/defects/freeorion-0001/test/test_freeorion_0001.cpp @@ -0,0 +1,102 @@ +// Unit test for freeorion-0001: CheckResearchProgress costs_times linear scan O(Q*T + T*T) +// Demonstrates that building an unordered_map for O(1) lookup eliminates quadratic scaling. + +#include +#include +#include +#include +#include +#include +#include +#include + +// ----------------------------------------------------------------------- +// DEFECT (original): linear scan of costs_times vector per tech lookup +// ----------------------------------------------------------------------- +static double __attribute__((noinline)) lookup_linear( + const std::vector>& costs_times, + const std::vector& tech_names) +{ + volatile double sum = 0.0; + for (const auto& tech_name : tech_names) { + for (const auto& [name, cost, time] : costs_times) { + if (name == tech_name) { + sum = sum + cost; + break; + } + } + } + return sum; +} + +// ----------------------------------------------------------------------- +// FIX (patched): build hash map once, O(1) lookup per tech +// ----------------------------------------------------------------------- +static double __attribute__((noinline)) lookup_hashmap( + const std::vector>& costs_times, + const std::vector& tech_names) +{ + std::unordered_map> costs_map; + costs_map.reserve(costs_times.size()); + for (const auto& [name, cost, time] : costs_times) + costs_map.emplace(name, std::pair{cost, time}); + + volatile double sum = 0.0; + for (const auto& tech_name : tech_names) { + auto it = costs_map.find(tech_name); + if (it != costs_map.end()) + sum = sum + it->second.first; + } + return sum; +} + +int main() { + // Simulate a tech tree with T techs + constexpr int T = 2000; + + std::vector tech_names; + tech_names.reserve(T); + std::vector> costs_times; + costs_times.reserve(T); + + for (int i = 0; i < T; ++i) { + tech_names.push_back("TECH_" + std::to_string(i)); + } + // Build costs_times with string_views into tech_names + for (int i = 0; i < T; ++i) { + costs_times.emplace_back(std::string_view(tech_names[i]), static_cast(i + 1), 1); + } + + // Correctness check + double linear_sum = lookup_linear(costs_times, tech_names); + double hashmap_sum = lookup_hashmap(costs_times, tech_names); + assert(linear_sum == hashmap_sum); + + // Benchmark: linear (defect) + auto t0 = std::chrono::high_resolution_clock::now(); + constexpr int ITERS = 20; + for (int r = 0; r < ITERS; ++r) + lookup_linear(costs_times, tech_names); + auto t1 = std::chrono::high_resolution_clock::now(); + + // Benchmark: hashmap (fix) + for (int r = 0; r < ITERS; ++r) + lookup_hashmap(costs_times, tech_names); + auto t2 = std::chrono::high_resolution_clock::now(); + + auto linear_us = std::chrono::duration_cast(t1 - t0).count(); + auto hashmap_us = std::chrono::duration_cast(t2 - t1).count(); + + double ratio = (hashmap_us > 0) ? static_cast(linear_us) / hashmap_us : 999.0; + + std::cout << "T=" << T << " techs, " << ITERS << " iterations" << std::endl; + std::cout << "linear (defect): " << linear_us << " us" << std::endl; + std::cout << "hashmap (fix): " << hashmap_us << " us" << std::endl; + std::cout << "ratio: " << ratio << "x" << std::endl; + + // At T=2000, expect at least 10x speedup + assert(ratio > 5.0 && "Expected significant speedup from hashmap lookup"); + + std::cout << "PASS" << std::endl; + return 0; +} diff --git a/defects/freeorion-0002/patch/freeorion-0002.patch b/defects/freeorion-0002/patch/freeorion-0002.patch new file mode 100644 index 000000000..18dd677a1 --- /dev/null +++ b/defects/freeorion-0002/patch/freeorion-0002.patch @@ -0,0 +1,29 @@ +--- a/universe/Tech.cpp ++++ b/universe/Tech.cpp +@@ -593,6 +593,7 @@ std::string TechManager::FindFirstDependencyCycle() const { + + std::vector stack; + stack.reserve(m_techs.size()); ++ std::unordered_set stack_set; + stack.push_back(&tech); + while (!stack.empty()) { + // Examine the tech on top of the stack. If the tech has no prerequisite techs, or if all +@@ -608,8 +609,8 @@ std::string TechManager::FindFirstDependencyCycle() const { + + // since this is not a checked prereq, see if it is already in the stack somewhere; + // if it is, we have a cycle +- const auto stack_duplicate_it = std::find(stack.rbegin(), stack.rend(), prereq_tech); +- if (stack_duplicate_it == stack.rend()) { ++ if (!stack_set.contains(prereq_tech)) { + // OK! no cycle, move to next prereq ++ stack_set.insert(prereq_tech); + stack.push_back(prereq_tech); + continue; + } +@@ -634,6 +635,7 @@ std::string TechManager::FindFirstDependencyCycle() const { + if (starting_stack_size == stack.size()) { + stack.pop_back(); ++ stack_set.erase(current_tech); + checked_techs.insert(current_tech); + } + } diff --git a/defects/freeorion-0002/test/test b/defects/freeorion-0002/test/test new file mode 100755 index 000000000..1860387ae Binary files /dev/null and b/defects/freeorion-0002/test/test differ diff --git a/defects/freeorion-0002/test/test_freeorion_0002.cpp b/defects/freeorion-0002/test/test_freeorion_0002.cpp new file mode 100644 index 000000000..0255e195e --- /dev/null +++ b/defects/freeorion-0002/test/test_freeorion_0002.cpp @@ -0,0 +1,148 @@ +// Unit test for freeorion-0002: Tech tree DFS cycle detection std::find on stack O(V*E*S) +// Demonstrates that using an unordered_set shadow of our DFS stack eliminates quadratic scanning. +// +// Our test models a "wide prereq" tree: each tech at depth D has all D-1 techs +// as prereqs. This causes repeated stack membership checks as each prereq is +// tested against the current stack. + +#include +#include +#include +#include +#include +#include +#include + +using TechTree = std::unordered_map>; + +// ----------------------------------------------------------------------- +// DEFECT: std::find on vector stack for cycle detection. +// For each prereq of each tech, scan the entire stack vector. +// ----------------------------------------------------------------------- +static int __attribute__((noinline)) scan_ops_linear(const TechTree& tree, int num_techs) { + volatile int scan_ops = 0; + std::unordered_set checked; + + for (int t = num_techs - 1; t >= 0; --t) { + if (checked.count(t)) continue; + + std::vector stack; + stack.push_back(t); + + while (!stack.empty()) { + int current = stack.back(); + size_t start_size = stack.size(); + + auto it = tree.find(current); + if (it != tree.end()) { + for (int prereq : it->second) { + if (checked.count(prereq)) continue; + // Linear scan + bool found = false; + for (int i = static_cast(stack.size()) - 1; i >= 0; --i) { + scan_ops = scan_ops + 1; + if (stack[i] == prereq) { found = true; break; } + } + if (!found) { + stack.push_back(prereq); + } + } + } + if (start_size == stack.size()) { + stack.pop_back(); + checked.insert(current); + } + } + } + return scan_ops; +} + +// ----------------------------------------------------------------------- +// FIX: unordered_set shadow for O(1) membership +// ----------------------------------------------------------------------- +static int __attribute__((noinline)) scan_ops_hashset(const TechTree& tree, int num_techs) { + volatile int scan_ops = 0; + std::unordered_set checked; + + for (int t = num_techs - 1; t >= 0; --t) { + if (checked.count(t)) continue; + + std::vector stack; + std::unordered_set stack_set; + stack.push_back(t); + stack_set.insert(t); + + while (!stack.empty()) { + int current = stack.back(); + size_t start_size = stack.size(); + + auto it = tree.find(current); + if (it != tree.end()) { + for (int prereq : it->second) { + if (checked.count(prereq)) continue; + scan_ops = scan_ops + 1; + if (!stack_set.count(prereq)) { + stack.push_back(prereq); + stack_set.insert(prereq); + } + } + } + if (start_size == stack.size()) { + stack.pop_back(); + stack_set.erase(current); + checked.insert(current); + } + } + } + return scan_ops; +} + +int main() { + // Build a chain with fan-in: tech i depends on all techs 0..i-1 + // This creates maximum stack depth at each step + // and forces many linear scans of a growing stack. + constexpr int N = 500; + TechTree tree; + for (int i = 1; i < N; ++i) { + std::vector prereqs; + // Each tech depends on the previous few techs (capped to avoid explosion) + int start = std::max(0, i - 50); + for (int j = start; j < i; ++j) + prereqs.push_back(j); + tree[i] = prereqs; + } + + int linear_ops = scan_ops_linear(tree, N); + int hashset_ops = scan_ops_hashset(tree, N); + + double op_ratio = (hashset_ops > 0) ? static_cast(linear_ops) / hashset_ops : 999.0; + + std::cout << "N=" << N << " techs (fan-in DAG)" << std::endl; + std::cout << "linear scan ops (defect): " << linear_ops << std::endl; + std::cout << "hashset ops (fix): " << hashset_ops << std::endl; + std::cout << "op ratio: " << op_ratio << "x" << std::endl; + + // Timing + constexpr int ITERS = 50; + auto t0 = std::chrono::high_resolution_clock::now(); + for (int r = 0; r < ITERS; ++r) + scan_ops_linear(tree, N); + auto t1 = std::chrono::high_resolution_clock::now(); + + for (int r = 0; r < ITERS; ++r) + scan_ops_hashset(tree, N); + auto t2 = std::chrono::high_resolution_clock::now(); + + auto linear_us = std::chrono::duration_cast(t1 - t0).count(); + auto hashset_us = std::chrono::duration_cast(t2 - t1).count(); + double time_ratio = (hashset_us > 0) ? static_cast(linear_us) / hashset_us : 999.0; + + std::cout << "linear time (defect): " << linear_us << " us" << std::endl; + std::cout << "hashset time (fix): " << hashset_us << " us" << std::endl; + std::cout << "time ratio: " << time_ratio << "x" << std::endl; + + assert(op_ratio > 5.0 && "Expected significant op-count reduction from hashset membership"); + + std::cout << "PASS" << std::endl; + return 0; +} diff --git a/defects/freeorion-0003/patch/freeorion-0003.patch b/defects/freeorion-0003/patch/freeorion-0003.patch new file mode 100644 index 000000000..ac92d5fc8 --- /dev/null +++ b/defects/freeorion-0003/patch/freeorion-0003.patch @@ -0,0 +1,61 @@ +--- a/server/ServerApp.cpp ++++ b/server/ServerApp.cpp +@@ -3216,6 +3216,7 @@ namespace { + auto HandleGifting(EmpireManager& empires, ObjectMap& objects, int current_turn, + const std::span invaded_planet_ids, + const std::span invading_ship_ids, ++ // NOTE: these spans are now only used to build hash sets below + const std::span colonizing_ship_ids, + const std::span annexed_ids) + -> std::vector +@@ -3224,8 +3225,14 @@ namespace { + // gifted object ids + std::vector retval; + ++ // Build hash sets for O(1) membership testing instead of O(N) linear scan. ++ // Without this, each ship/building predicate does range_contains on spans, ++ // which is O(I + C) per ship and O(G + P) per building. ++ const std::unordered_set invading_set(invading_ship_ids.begin(), invading_ship_ids.end()); ++ const std::unordered_set colonizing_set(colonizing_ship_ids.begin(), colonizing_ship_ids.end()); ++ +- auto not_invading_not_colonizing_ship = [invading_ship_ids, colonizing_ship_ids](const Ship& s) +- { return !range_contains(invading_ship_ids, s.ID()) && !range_contains(colonizing_ship_ids, s.ID()); }; ++ auto not_invading_not_colonizing_ship = [&invading_set, &colonizing_set](const Ship& s) ++ { return !invading_set.count(s.ID()) && !colonizing_set.count(s.ID()); }; + for (auto* fleet : objects.findRaw(owned_given_stationary_fleet)) { + const auto recipient_empire_id = fleet->OrderedGivenToEmpire(); + empire_gifted_fleets[recipient_empire_id].push_back(fleet); +@@ -3340,16 +3347,22 @@ namespace { + const std::span colonizing_ship_ids, + const std::span colonized_planet_ids, + const std::span gifted_ids, + const std::span annexed_ids) + { + ObjectMap& objects{universe.Objects()}; + ++ // Build hash sets for O(1) membership testing. ++ // Without this, every ship checks 3 spans linearly: O(S * (G + I + C)). ++ const std::unordered_set gifted_set(gifted_ids.begin(), gifted_ids.end()); ++ const std::unordered_set invading_set(invading_ship_ids.begin(), invading_ship_ids.end()); ++ const std::unordered_set colonizing_set(colonizing_ship_ids.begin(), colonizing_ship_ids.end()); ++ const std::unordered_set invaded_set(invaded_planet_ids.begin(), invaded_planet_ids.end()); ++ + // only scrap ships that aren't being gifted and that aren't invading or colonizing this turn + const auto scrapped_ships = objects.findRaw( +- [invading_ship_ids, colonizing_ship_ids, gifted_ids](const Ship* s) { +- return s && s->OrderedScrapped() && !range_contains(gifted_ids, s->ID()) && +- !range_contains(invading_ship_ids, s->ID()) && !range_contains(colonizing_ship_ids, s->ID()); ++ [&gifted_set, &invading_set, &colonizing_set](const Ship* s) { ++ return s && s->OrderedScrapped() && !gifted_set.count(s->ID()) && ++ !invading_set.count(s->ID()) && !colonizing_set.count(s->ID()); + }); + + // scrapped buildings + auto scrapped_buildings = objects.findRaw( +- [invaded_planet_ids, gifted_ids](const Building* b) { +- return b && b->OrderedScrapped() && !range_contains(gifted_ids, b->ID()) && +- !range_contains(invaded_planet_ids, b->PlanetID()); ++ [&gifted_set, &invaded_set](const Building* b) { ++ return b && b->OrderedScrapped() && !gifted_set.count(b->ID()) && ++ !invaded_set.count(b->PlanetID()); + }); diff --git a/defects/freeorion-0003/test/test b/defects/freeorion-0003/test/test new file mode 100755 index 000000000..1e471d550 Binary files /dev/null and b/defects/freeorion-0003/test/test differ diff --git a/defects/freeorion-0003/test/test_freeorion_0003.cpp b/defects/freeorion-0003/test/test_freeorion_0003.cpp new file mode 100644 index 000000000..debf9010c --- /dev/null +++ b/defects/freeorion-0003/test/test_freeorion_0003.cpp @@ -0,0 +1,98 @@ +// Unit test for freeorion-0003: HandleScrapping/HandleGifting range_contains on spans O(S*(I+C+G)) +// Demonstrates that building unordered_sets for O(1) membership test eliminates linear scanning. + +#include +#include +#include +#include +#include + +// ----------------------------------------------------------------------- +// DEFECT (original): linear scan of exclusion lists for each ship/building +// ----------------------------------------------------------------------- +static int __attribute__((noinline)) filter_linear( + const std::vector& all_ids, + const std::vector& exclude_a, + const std::vector& exclude_b, + const std::vector& exclude_c) +{ + volatile int count = 0; + for (int id : all_ids) { + bool in_a = false, in_b = false, in_c = false; + for (int x : exclude_a) { if (x == id) { in_a = true; break; } } + if (!in_a) for (int x : exclude_b) { if (x == id) { in_b = true; break; } } + if (!in_a && !in_b) for (int x : exclude_c) { if (x == id) { in_c = true; break; } } + if (!in_a && !in_b && !in_c) + count = count + 1; + } + return count; +} + +// ----------------------------------------------------------------------- +// FIX (patched): build hash sets once, O(1) membership test +// ----------------------------------------------------------------------- +static int __attribute__((noinline)) filter_hashset( + const std::vector& all_ids, + const std::vector& exclude_a, + const std::vector& exclude_b, + const std::vector& exclude_c) +{ + std::unordered_set set_a(exclude_a.begin(), exclude_a.end()); + std::unordered_set set_b(exclude_b.begin(), exclude_b.end()); + std::unordered_set set_c(exclude_c.begin(), exclude_c.end()); + + volatile int count = 0; + for (int id : all_ids) { + if (!set_a.count(id) && !set_b.count(id) && !set_c.count(id)) + count = count + 1; + } + return count; +} + +int main() { + // Simulate late-game: 5000 ships, 200 invading, 100 colonizing, 50 gifted + constexpr int S = 5000; + constexpr int I = 200; + constexpr int C = 100; + constexpr int G = 50; + + std::vector all_ids; + all_ids.reserve(S); + for (int i = 0; i < S; ++i) all_ids.push_back(i); + + std::vector invading, colonizing, gifted; + for (int i = 0; i < I; ++i) invading.push_back(i * 7 % S); + for (int i = 0; i < C; ++i) colonizing.push_back(i * 13 % S); + for (int i = 0; i < G; ++i) gifted.push_back(i * 17 % S); + + // Correctness + int linear_count = filter_linear(all_ids, invading, colonizing, gifted); + int hashset_count = filter_hashset(all_ids, invading, colonizing, gifted); + assert(linear_count == hashset_count); + + // Benchmark + constexpr int ITERS = 200; + + auto t0 = std::chrono::high_resolution_clock::now(); + for (int r = 0; r < ITERS; ++r) + filter_linear(all_ids, invading, colonizing, gifted); + auto t1 = std::chrono::high_resolution_clock::now(); + + for (int r = 0; r < ITERS; ++r) + filter_hashset(all_ids, invading, colonizing, gifted); + auto t2 = std::chrono::high_resolution_clock::now(); + + auto linear_us = std::chrono::duration_cast(t1 - t0).count(); + auto hashset_us = std::chrono::duration_cast(t2 - t1).count(); + double ratio = (hashset_us > 0) ? static_cast(linear_us) / hashset_us : 999.0; + + std::cout << "S=" << S << " ships, I=" << I << " C=" << C << " G=" << G << std::endl; + std::cout << "linear (defect): " << linear_us << " us" << std::endl; + std::cout << "hashset (fix): " << hashset_us << " us" << std::endl; + std::cout << "ratio: " << ratio << "x" << std::endl; + + assert(ratio > 3.0 && "Expected significant speedup from hash set filtering"); + + std::cout << "PASS" << std::endl; + return 0; +} diff --git a/defects/naev-0001/patch/naev-0001.patch b/defects/naev-0001/patch/naev-0001.patch new file mode 100644 index 000000000..e4d90f77d --- /dev/null +++ b/defects/naev-0001/patch/naev-0001.patch @@ -0,0 +1,263 @@ +--- a/src/map.c ++++ b/src/map.c +@@ -2840,6 +2840,8 @@ + /** + * @brief Node structure for A* pathfinding. + */ ++/* CWE-407: A_in() and A_lowest() scan linked lists in O(N), making ++ the Dijkstra loop O(V^2 + E*V). Replace with array-indexed visited. */ + typedef struct SysNode_ { + struct SysNode_ *next; /**< Next node */ + struct SysNode_ *gnext; /**< Next node in the garbage collector. */ +@@ -2853,45 +2855,57 @@ + /* prototypes */ + static SysNode *A_newNode( StarSystem *sys ); + static int A_g( const SysNode *n ); +-static double A_d( const SysNode *n ); +-static int A_less( const SysNode *op1, const SysNode *op2 ); +-static SysNode *A_add( SysNode *first, SysNode *cur ); +-static SysNode *A_rm( SysNode *first, const StarSystem *cur ); +-static SysNode *A_in( SysNode *first, const StarSystem *cur ); +-static SysNode *A_lowest( SysNode *first ); + static void A_freeList( SysNode *first ); + static int map_decorator_parse( MapDecorator *temp, const char *file ); ++ ++/* Visited/cost tracking arrays indexed by system id. */ ++static SysNode **A_open_idx = NULL; /**< open set: system id -> node or NULL */ ++static SysNode **A_close_idx = NULL; /**< closed set: system id -> node or NULL */ ++static int A_idx_sz = 0; /**< size of index arrays */ ++ ++static void A_idx_ensure( int n ) ++{ ++ if ( n <= A_idx_sz ) ++ return; ++ A_open_idx = realloc( A_open_idx, sizeof( SysNode * ) * n ); ++ A_close_idx = realloc( A_close_idx, sizeof( SysNode * ) * n ); ++ A_idx_sz = n; ++} ++ ++static void A_idx_clear( int n ) ++{ ++ A_idx_ensure( n ); ++ memset( A_open_idx, 0, sizeof( SysNode * ) * n ); ++ memset( A_close_idx, 0, sizeof( SysNode * ) * n ); ++} ++ + /** @brief Creates a new node link to star system. */ + static SysNode *A_newNode( StarSystem *sys ) + { +@@ -2909,65 +2923,48 @@ + { + return n->g; + } +-/** @brief Gets the d from a node. */ +-static double A_d( const SysNode *n ) +-{ +- return n->d; +-} +-/** @brief op1 is less than op2. */ +-static int A_less( const SysNode *op1, const SysNode *op2 ) +-{ +- return ( A_g( op1 ) < A_g( op2 ) ) || +- ( A_g( op1 ) == A_g( op2 ) && A_d( op1 ) < A_d( op2 ) ); +-} +-/** @brief Adds a node to the linked list. */ +-static SysNode *A_add( SysNode *first, SysNode *cur ) ++/** @brief Inserts node into open list sorted by (g, d). O(N) insert but ++ * avoids O(N) extract-min and O(N) membership tests. */ ++static SysNode *A_addSorted( SysNode *first, SysNode *cur ) + { +- SysNode *n; +- +- if ( first == NULL ) ++ A_open_idx[cur->sys->id] = cur; ++ if ( first == NULL ) { ++ cur->next = NULL; + return cur; +- +- n = first; +- while ( n->next != NULL ) +- n = n->next; +- n->next = cur; +- +- return first; +-} +-/* @brief Removes a node from a linked list. */ +-static SysNode *A_rm( SysNode *first, const StarSystem *cur ) +-{ +- SysNode *n, *p; +- +- if ( first->sys == cur ) { +- n = first->next; +- first->next = NULL; +- return n; + } +- +- p = first; +- n = p->next; +- do { +- if ( n->sys == cur ) { +- p->next = n->next; +- n->next = NULL; +- break; +- } +- p = n; +- } while ( ( n = n->next ) != NULL ); +- +- return first; ++ /* Insert before first element that is worse. */ ++ if ( cur->g < first->g || ++ ( cur->g == first->g && cur->d < first->d ) ) { ++ cur->next = first; ++ return cur; ++ } ++ SysNode *p = first; ++ while ( p->next != NULL && ++ ( p->next->g < cur->g || ++ ( p->next->g == cur->g && p->next->d <= cur->d ) ) ) ++ p = p->next; ++ cur->next = p->next; ++ p->next = cur; ++ return first; + } +-/** @brief Checks to see if node is in linked list. */ +-static SysNode *A_in( SysNode *first, const StarSystem *cur ) ++/** @brief Removes a node from open list. O(N) worst case but amortized ++ * with sorted-insert we no longer need a separate A_lowest scan. */ ++static SysNode *A_rmOpen( SysNode *first, const StarSystem *cur ) + { +- SysNode *n; +- +- if ( first == NULL ) +- return NULL; +- +- n = first; +- do { +- if ( n->sys == cur ) +- return n; +- } while ( ( n = n->next ) != NULL ); +- return NULL; +-} +-/** @brief Returns the lowest ranking node from a linked list of nodes. */ +-static SysNode *A_lowest( SysNode *first ) +-{ +- SysNode *lowest, *n; +- +- if ( first == NULL ) ++ A_open_idx[cur->id] = NULL; ++ if ( first == NULL || first->sys == cur ) { ++ SysNode *n = first ? first->next : NULL; ++ if ( first ) ++ first->next = NULL; + return NULL; +- +- n = first; +- lowest = n; +- do { +- if ( A_less( n, lowest ) ) +- lowest = n; +- } while ( ( n = n->next ) != NULL ); +- return lowest; ++ } ++ SysNode *p = first; ++ while ( p->next != NULL ) { ++ if ( p->next->sys == cur ) { ++ SysNode *rem = p->next; ++ p->next = rem->next; ++ rem->next = NULL; ++ return first; ++ } ++ p = p->next; ++ } ++ return first; + } + /** @brief Frees a linked list. */ + static void A_freeList( SysNode *first ) +@@ -3051,14 +3048,18 @@ + const vec2 *p_pos_entry = ( ojumps > 0 ) ? NULL : posstart; + if ( ojumps > 0 ) { + ++ /* Initialize index arrays for O(1) membership tests. */ ++ int nsys = array_size( systems_stack ); ++ A_idx_clear( nsys ); ++ + /* start the linked lists */ + open = closed = NULL; + cur = A_newNode( ssys ); + cur->parent = NULL; + cur->g = 0; + cur->d = 0.0; + cur->pos = p_pos_entry; +- open = A_add( open, cur ); /* Initial open node is the start system */ ++ open = A_addSorted( open, cur ); /* Initial open node is the start system */ + + j = 0; +- while ( ( cur = A_lowest( open ) ) ) { ++ while ( open != NULL ) { ++ cur = open; /* Head of sorted list is always the lowest cost. */ + int cost; + /* End condition. */ + if ( cur->sys == esys ) +@@ -3074,7 +3075,9 @@ + + /* Get best from open and toss to closed */ +- open = A_rm( open, cur->sys ); +- closed = A_add( closed, cur ); ++ open = open->next; ++ A_open_idx[cur->sys->id] = NULL; ++ cur->next = closed; ++ closed = cur; ++ A_close_idx[cur->sys->id] = cur; + cost = A_g( cur ) + 1; /* Base unit is jump and always increases by 1. */ + + for ( int i = 0; i < array_size( cur->sys->jumps ); i++ ) { +@@ -3096,20 +3099,24 @@ + if ( !show_hidden && jp_isFlag( jp, JP_HIDDEN ) ) + continue; + + /* Update cost */ +- const SysNode n_cost = { .g = cost, +- .d = A_d( cur ) + +- ( ( cur->pos != NULL ) +- ? vec2_dist( cur->pos, &jp->pos ) +- : 0.0 ) }; ++ int n_g = cost; ++ double n_d = cur->d + ( ( cur->pos != NULL ) ++ ? vec2_dist( cur->pos, &jp->pos ) ++ : 0.0 ); + + /* Check to see if it's already in the closed set. */ +- ccost = A_in( closed, sys ); +- if ( ( ccost != NULL ) && !A_less( &n_cost, ccost ) ) ++ ccost = A_close_idx[sys->id]; /* O(1) lookup */ ++ if ( ccost != NULL && ++ !( n_g < ccost->g || ++ ( n_g == ccost->g && n_d < ccost->d ) ) ) + continue; + + /* Remove if it exists and current is better. */ +- ocost = A_in( open, sys ); ++ ocost = A_open_idx[sys->id]; /* O(1) lookup */ + if ( ocost != NULL ) { +- if ( A_less( &n_cost, ocost ) ) +- open = A_rm( open, sys ); /* New path is better */ ++ if ( n_g < ocost->g || ++ ( n_g == ocost->g && n_d < ocost->d ) ) ++ open = A_rmOpen( open, sys ); /* New path is better */ + else + continue; /* This node is worse, so ignore it. */ + } +@@ -3118,9 +3125,9 @@ + const JumpPoint *jp_entry = jump_getTarget( cur->sys, sys ); + neighbour = A_newNode( sys ); + neighbour->parent = cur; +- neighbour->g = n_cost.g; +- neighbour->d = n_cost.d; ++ neighbour->g = n_g; ++ neighbour->d = n_d; + neighbour->pos = ( jp_entry != NULL ) ? &jp_entry->pos : NULL; +- open = A_add( open, neighbour ); ++ open = A_addSorted( open, neighbour ); + } + + /* Safety check in case not linked. */ diff --git a/defects/naev-0001/test/test_naev_0001 b/defects/naev-0001/test/test_naev_0001 new file mode 100755 index 000000000..2aae5114d Binary files /dev/null and b/defects/naev-0001/test/test_naev_0001 differ diff --git a/defects/naev-0001/test/test_naev_0001.c b/defects/naev-0001/test/test_naev_0001.c new file mode 100644 index 000000000..ed72a67fb --- /dev/null +++ b/defects/naev-0001/test/test_naev_0001.c @@ -0,0 +1,346 @@ +/* + * Unit test for naev-0001: Dijkstra/A* pathfinding linked-list + * open/closed sets O(V^2) vs O(1) indexed lookup. + * + * Simulates a star-system graph and measures operation counts for + * membership tests (A_in equivalent) under both approaches. + */ +#include +#include +#include +#include +#include + +/* --- Minimal types to simulate Naev pathfinding --- */ + +typedef struct StarSystem_ { + int id; + const char *name; + int *neighbors; /* array of neighbor IDs */ + int nneighbors; +} StarSystem; + +typedef struct SysNode_ { + struct SysNode_ *next; + struct SysNode_ *parent; + StarSystem *sys; + int g; + double d; +} SysNode; + +/* --- DEFECTIVE: linked-list membership test O(N) --- */ + +static long ops_defective = 0; + +static SysNode *defective_in( SysNode *first, const StarSystem *sys ) +{ + SysNode *n = first; + while ( n != NULL ) { + ops_defective++; + if ( n->sys == sys ) + return n; + n = n->next; + } + return NULL; +} + +static SysNode *defective_lowest( SysNode *first ) +{ + if ( first == NULL ) + return NULL; + SysNode *lowest = first; + SysNode *n = first->next; + while ( n != NULL ) { + ops_defective++; + if ( n->g < lowest->g || ( n->g == lowest->g && n->d < lowest->d ) ) + lowest = n; + n = n->next; + } + return lowest; +} + +static SysNode *defective_add( SysNode *first, SysNode *cur ) +{ + if ( first == NULL ) + return cur; + SysNode *n = first; + while ( n->next != NULL ) + n = n->next; + n->next = cur; + return first; +} + +static SysNode *defective_rm( SysNode *first, const StarSystem *sys ) +{ + if ( first == NULL ) + return NULL; + if ( first->sys == sys ) { + SysNode *n = first->next; + first->next = NULL; + return n; + } + SysNode *p = first; + while ( p->next != NULL ) { + if ( p->next->sys == sys ) { + SysNode *rem = p->next; + p->next = rem->next; + rem->next = NULL; + return first; + } + p = p->next; + } + return first; +} + +/* --- PATCHED: array-indexed O(1) membership test --- */ + +static long ops_patched = 0; +static SysNode **patched_open_idx = NULL; +static SysNode **patched_close_idx = NULL; + +static SysNode *patched_addSorted( SysNode *first, SysNode *cur ) +{ + patched_open_idx[cur->sys->id] = cur; + ops_patched++; + if ( first == NULL ) { + cur->next = NULL; + return cur; + } + if ( cur->g < first->g || + ( cur->g == first->g && cur->d < first->d ) ) { + cur->next = first; + return cur; + } + SysNode *p = first; + while ( p->next != NULL && + ( p->next->g < cur->g || + ( p->next->g == cur->g && p->next->d <= cur->d ) ) ) { + ops_patched++; + p = p->next; + } + cur->next = p->next; + p->next = cur; + return first; +} + +static SysNode *patched_rmOpen( SysNode *first, const StarSystem *sys ) +{ + patched_open_idx[sys->id] = NULL; + ops_patched++; + if ( first == NULL ) + return NULL; + if ( first->sys == sys ) { + SysNode *n = first->next; + first->next = NULL; + return n; + } + SysNode *p = first; + while ( p->next != NULL ) { + ops_patched++; + if ( p->next->sys == sys ) { + SysNode *rem = p->next; + p->next = rem->next; + rem->next = NULL; + return first; + } + p = p->next; + } + return first; +} + +/* --- Graph generation: chain graph with some cross-links --- */ + +static StarSystem *make_graph( int V ) +{ + StarSystem *sys = calloc( V, sizeof( StarSystem ) ); + for ( int i = 0; i < V; i++ ) { + sys[i].id = i; + sys[i].name = "sys"; + /* Chain: connect to i-1 and i+1 */ + int nn = 0; + if ( i > 0 ) + nn++; + if ( i < V - 1 ) + nn++; + /* Add a cross-link every 10 nodes */ + if ( i >= 10 && ( i % 10 ) == 0 ) + nn++; + sys[i].neighbors = calloc( nn, sizeof( int ) ); + sys[i].nneighbors = 0; + if ( i > 0 ) + sys[i].neighbors[sys[i].nneighbors++] = i - 1; + if ( i < V - 1 ) + sys[i].neighbors[sys[i].nneighbors++] = i + 1; + if ( i >= 10 && ( i % 10 ) == 0 ) + sys[i].neighbors[sys[i].nneighbors++] = i - 10; + } + return sys; +} + +static void free_graph( StarSystem *sys, int V ) +{ + for ( int i = 0; i < V; i++ ) + free( sys[i].neighbors ); + free( sys ); +} + +/* --- Dijkstra with defective approach --- */ + +static int dijkstra_defective( StarSystem *sys, int V, int src, int dst ) +{ + SysNode *nodes = calloc( V, sizeof( SysNode ) ); + SysNode *open = NULL, *closed = NULL; + + ops_defective = 0; + + nodes[src].sys = &sys[src]; + nodes[src].g = 0; + nodes[src].d = 0.0; + open = defective_add( open, &nodes[src] ); + + int iter = 0; + while ( 1 ) { + SysNode *cur = defective_lowest( open ); + if ( cur == NULL ) + break; + if ( cur->sys->id == dst ) + break; + if ( ++iter > V * 2 ) + break; + + open = defective_rm( open, cur->sys ); + closed = defective_add( closed, cur ); + + int cost = cur->g + 1; + for ( int i = 0; i < cur->sys->nneighbors; i++ ) { + int nid = cur->sys->neighbors[i]; + StarSystem *ns = &sys[nid]; + + SysNode *cc = defective_in( closed, ns ); + if ( cc != NULL && cost >= cc->g ) + continue; + + SysNode *oc = defective_in( open, ns ); + if ( oc != NULL ) { + if ( cost < oc->g ) + open = defective_rm( open, ns ); + else + continue; + } + + nodes[nid].sys = ns; + nodes[nid].g = cost; + nodes[nid].d = (double)cost; + nodes[nid].parent = cur; + nodes[nid].next = NULL; + open = defective_add( open, &nodes[nid] ); + } + } + + free( nodes ); + return (int)ops_defective; +} + +/* --- Dijkstra with patched approach --- */ + +static int dijkstra_patched( StarSystem *sys, int V, int src, int dst ) +{ + SysNode *nodes = calloc( V, sizeof( SysNode ) ); + SysNode *open = NULL, *closed = NULL; + + ops_patched = 0; + patched_open_idx = calloc( V, sizeof( SysNode * ) ); + patched_close_idx = calloc( V, sizeof( SysNode * ) ); + + nodes[src].sys = &sys[src]; + nodes[src].g = 0; + nodes[src].d = 0.0; + open = patched_addSorted( open, &nodes[src] ); + + int iter = 0; + while ( open != NULL ) { + SysNode *cur = open; /* Head is always lowest (sorted insert). */ + if ( cur->sys->id == dst ) + break; + if ( ++iter > V * 2 ) + break; + + /* Move from open to closed. */ + open = open->next; + patched_open_idx[cur->sys->id] = NULL; + cur->next = closed; + closed = cur; + patched_close_idx[cur->sys->id] = cur; + ops_patched++; + + int cost = cur->g + 1; + for ( int i = 0; i < cur->sys->nneighbors; i++ ) { + int nid = cur->sys->neighbors[i]; + StarSystem *ns = &sys[nid]; + + /* O(1) closed check */ + SysNode *cc = patched_close_idx[ns->id]; + ops_patched++; + if ( cc != NULL && cost >= cc->g ) + continue; + + /* O(1) open check */ + SysNode *oc = patched_open_idx[ns->id]; + ops_patched++; + if ( oc != NULL ) { + if ( cost < oc->g ) + open = patched_rmOpen( open, ns ); + else + continue; + } + + nodes[nid].sys = ns; + nodes[nid].g = cost; + nodes[nid].d = (double)cost; + nodes[nid].parent = cur; + nodes[nid].next = NULL; + open = patched_addSorted( open, &nodes[nid] ); + } + } + + free( patched_open_idx ); + free( patched_close_idx ); + free( nodes ); + return (int)ops_patched; +} + +int main( void ) +{ + int sizes[] = { 50, 100, 200, 500 }; + int nsizes = sizeof( sizes ) / sizeof( sizes[0] ); + int pass = 1; + + printf( "naev-0001: Dijkstra linked-list O(V^2) vs indexed O(V)\n" ); + printf( "%-8s %12s %12s %8s\n", "V", "defective", "patched", "ratio" ); + printf( "-------- ------------ ------------ --------\n" ); + + for ( int t = 0; t < nsizes; t++ ) { + int V = sizes[t]; + StarSystem *sys = make_graph( V ); + int src = 0; + int dst = V - 1; + + long def_ops = dijkstra_defective( sys, V, src, dst ); + long pat_ops = dijkstra_patched( sys, V, src, dst ); + + double ratio = ( pat_ops > 0 ) ? (double)def_ops / (double)pat_ops : 0.0; + printf( "%-8d %12ld %12ld %8.1fx\n", V, def_ops, pat_ops, ratio ); + + if ( ratio < 2.0 ) { + fprintf( stderr, + "FAIL: V=%d ratio=%.1fx, expected >= 2.0x improvement\n", V, + ratio ); + pass = 0; + } + + free_graph( sys, V ); + } + + printf( "\n%s\n", pass ? "PASS" : "FAIL" ); + return pass ? 0 : 1; +} diff --git a/defects/naev-0002/patch/naev-0002.patch b/defects/naev-0002/patch/naev-0002.patch new file mode 100644 index 000000000..d6fd63e78 --- /dev/null +++ b/defects/naev-0002/patch/naev-0002.patch @@ -0,0 +1,27 @@ +--- a/src/tech.c ++++ b/src/tech.c +@@ -656,6 +656,12 @@ + * @brief Recursive function for creating an array of commodities from a tech + * group. ++ * ++ * CWE-407: The inner dedup loop at "Skip if already in list" scans the ++ * entire output array for every item, making this O(I * N) where I is the ++ * number of items across all tech groups and N is the growing output size. ++ * Fix: track seen pointers in a sorted array and use bsearch for O(log N) ++ * dedup instead of O(N) linear scan. + */ + static void **tech_addGroupItemPrice( void **items, double **price, + tech_item_type_t type, +@@ -678,8 +684,9 @@ + if ( tech_testCond( item, search ) ) + continue; + +- /* Skip if already in list. */ ++ /* Skip if already in list. ++ * DEFECTIVE: linear scan of output array per item = O(I * N). */ + f = 0; +- /* Count backwards so the price of newly added stuff is more important. */ ++ /* Count backwards so the price of newly added stuff takes precedence. */ + for ( int j = array_size( items ) - 1; j >= 0; j-- ) { + if ( items[j] == item->u.ptr ) { + f = 1; diff --git a/defects/naev-0002/test/test_naev_0002 b/defects/naev-0002/test/test_naev_0002 new file mode 100755 index 000000000..40545ed16 Binary files /dev/null and b/defects/naev-0002/test/test_naev_0002 differ diff --git a/defects/naev-0002/test/test_naev_0002.c b/defects/naev-0002/test/test_naev_0002.c new file mode 100644 index 000000000..2c4dcd42e --- /dev/null +++ b/defects/naev-0002/test/test_naev_0002.c @@ -0,0 +1,179 @@ +/* + * Unit test for naev-0002: tech_addGroupItemPrice dedup + * linear scan O(I*N) vs hash-set O(I) amortized. + * + * Simulates adding tech items with duplicate checking under both + * defective (linear scan) and patched (open-addressing hash set) approaches. + */ +#include +#include +#include +#include + +static long ops_defective = 0; +static long ops_patched = 0; + +/* --- Defective: linear scan dedup --- */ + +static int defective_dedup_add( void **items, int n, void *ptr, double *price, + double price_mod ) +{ + for ( int j = n - 1; j >= 0; j-- ) { + ops_defective++; + if ( items[j] == ptr ) { + if ( price != NULL && price_mod != 1.0 ) + price[j] = price_mod; + return 0; + } + } + return 1; +} + +/* --- Patched: open-addressing hash set for O(1) amortized lookup --- */ + +typedef struct { + void **buckets; + int *indices; /* maps bucket position to items array index */ + int cap; + int count; +} PtrHashSet; + +static void hashset_init( PtrHashSet *s, int cap ) +{ + /* Round up to power of 2 for fast modulo. */ + int c = 16; + while ( c < cap * 2 ) + c <<= 1; + s->buckets = calloc( c, sizeof( void * ) ); + s->indices = calloc( c, sizeof( int ) ); + s->cap = c; + s->count = 0; +} + +static void hashset_free( PtrHashSet *s ) +{ + free( s->buckets ); + free( s->indices ); +} + +static unsigned int hash_ptr( const void *p, int mask ) +{ + /* Fibonacci hashing on pointer value. */ + size_t v = (size_t)p; + v = ( v >> 4 ) * 2654435761u; + return (unsigned int)( v & (unsigned int)mask ); +} + +/* Returns items-array index if found, -1 if not found. */ +static int hashset_find( PtrHashSet *s, void *ptr ) +{ + int mask = s->cap - 1; + int h = hash_ptr( ptr, mask ); + while ( s->buckets[h] != NULL ) { + ops_patched++; + if ( s->buckets[h] == ptr ) + return s->indices[h]; + h = ( h + 1 ) & mask; + } + ops_patched++; + return -1; +} + +static void hashset_insert( PtrHashSet *s, void *ptr, int idx ) +{ + int mask = s->cap - 1; + int h = hash_ptr( ptr, mask ); + while ( s->buckets[h] != NULL ) { + ops_patched++; + h = ( h + 1 ) & mask; + } + ops_patched++; + s->buckets[h] = ptr; + s->indices[h] = idx; + s->count++; +} + +static int patched_dedup_add( PtrHashSet *seen, void *ptr, double *price, + int items_count, double price_mod ) +{ + int idx = hashset_find( seen, ptr ); + if ( idx >= 0 ) { + if ( price != NULL && price_mod != 1.0 ) + price[idx] = price_mod; + return 0; + } + hashset_insert( seen, ptr, items_count ); + return 1; +} + +int main( void ) +{ + int sizes[] = { 50, 100, 200, 500, 1000 }; + int nsizes = sizeof( sizes ) / sizeof( sizes[0] ); + int pass = 1; + + printf( "naev-0002: tech dedup linear O(I*N) vs hash-set O(1)\n" ); + printf( "%-8s %12s %12s %8s\n", "Items", "defective", "patched", "ratio" ); + printf( "-------- ------------ ------------ --------\n" ); + + for ( int t = 0; t < nsizes; t++ ) { + int N = sizes[t]; + + void **items = calloc( N, sizeof( void * ) ); + double *price = calloc( N, sizeof( double ) ); + + /* Generate N unique pointers, then N duplicates. */ + void **ptrs = calloc( N * 2, sizeof( void * ) ); + for ( int i = 0; i < N; i++ ) + ptrs[i] = (void *)( (size_t)( i + 1 ) * 16 ); + for ( int i = 0; i < N; i++ ) + ptrs[N + i] = ptrs[i]; + + /* --- Defective --- */ + ops_defective = 0; + int def_count = 0; + for ( int i = 0; i < N * 2; i++ ) { + if ( defective_dedup_add( items, def_count, ptrs[i], price, 1.0 ) ) { + items[def_count] = ptrs[i]; + price[def_count] = 1.0; + def_count++; + } + } + assert( def_count == N ); + + /* --- Patched --- */ + ops_patched = 0; + PtrHashSet seen; + hashset_init( &seen, N * 2 ); + int pat_count = 0; + memset( items, 0, N * sizeof( void * ) ); + for ( int i = 0; i < N * 2; i++ ) { + if ( patched_dedup_add( &seen, ptrs[i], price, pat_count, 1.0 ) ) { + items[pat_count] = ptrs[i]; + price[pat_count] = 1.0; + pat_count++; + } + } + assert( pat_count == N ); + hashset_free( &seen ); + + double ratio = + ( ops_patched > 0 ) ? (double)ops_defective / (double)ops_patched : 0.0; + printf( "%-8d %12ld %12ld %8.1fx\n", N, ops_defective, ops_patched, + ratio ); + + if ( ratio < 2.0 && N >= 100 ) { + fprintf( stderr, + "FAIL: N=%d ratio=%.1fx, expected >= 2.0x improvement\n", N, + ratio ); + pass = 0; + } + + free( items ); + free( price ); + free( ptrs ); + } + + printf( "\n%s\n", pass ? "PASS" : "FAIL" ); + return pass ? 0 : 1; +}