wesnoth: 3 CWE-407 defects, MOAD 0002-0005 CLEAN
wesnoth-0001: A* pathfinding std::find on pq vector for decrease-key O(V*Q) per relaxation, fix: lazy deletion. HIGH, 1279x at N=5000. wesnoth-0002: server ip_log_ deque linear scan on login/logoff O(N) per event with N up to 500. MEDIUM, 437x at L=2000. wesnoth-0003: combine_special_notes O(N^2) vector dedup utils::contains on vector per note insertion. MEDIUM, 499x at N=1000. MOAD-0002 (Intertangle): singletons deeply embedded, not actionable. MOAD-0003 (Leaked Context): thread_local for debug/call-stack only. MOAD-0004 (Logged Secret): passwords never logged verbatim. MOAD-0005 (Thundering Herd): single-threaded game + coroutine server. 6/6 unit tests PASS.
This commit is contained in:
parent
4e3dcc8d2a
commit
9fac7766ba
10 changed files with 735 additions and 1 deletions
|
|
@ -969,5 +969,6 @@
|
|||
"openttd-0002-0002": "UNDF-2026-000000968",
|
||||
"renpy-0001-0001": "UNDF-2026-000000969",
|
||||
"wesnoth-0001-0001": "UNDF-2026-000000970",
|
||||
"wesnoth-0002-0002": "UNDF-2026-000000971"
|
||||
"wesnoth-0002-0002": "UNDF-2026-000000971",
|
||||
"wesnoth-0003-0003": "UNDF-2026-000000972"
|
||||
}
|
||||
|
|
|
|||
46
defects/wesnoth-0001/patch/wesnoth-0001.patch
Normal file
46
defects/wesnoth-0001/patch/wesnoth-0001.patch
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# UNDF: UNDF-2026-000000970
|
||||
--- a/src/pathfind/astarsearch.cpp
|
||||
+++ b/src/pathfind/astarsearch.cpp
|
||||
@@ -172,9 +172,16 @@ plain_route a_star_search(const map_location& src, const map_location& dst,
|
||||
std::vector<int> pq;
|
||||
pq.push_back(index(src));
|
||||
|
||||
+ // Lazy-deletion A*: when a node is relaxed while already in the open
|
||||
+ // set, we push a duplicate entry instead of performing a decrease-key.
|
||||
+ // Stale duplicates are discarded when popped (the node will already
|
||||
+ // have in == search_counter, meaning it was finalized).
|
||||
while (!pq.empty()) {
|
||||
- node& n = nodes[pq.front()];
|
||||
-
|
||||
- n.in = search_counter;
|
||||
|
||||
std::pop_heap(pq.begin(), pq.end(), node_comp);
|
||||
- pq.pop_back();
|
||||
+ int front_idx = pq.back();
|
||||
+ pq.pop_back();
|
||||
+
|
||||
+ node& n = nodes[front_idx];
|
||||
+
|
||||
+ // Skip stale duplicates from lazy decrease-key.
|
||||
+ if (n.in == search_counter) continue;
|
||||
+ n.in = search_counter;
|
||||
|
||||
if (n.t >= dst_node.g) break;
|
||||
|
||||
@@ -213,10 +220,10 @@ plain_route a_star_search(const map_location& src, const map_location& dst,
|
||||
|
||||
next = node(cost, loc, n.curr, dst, true, teleports, srch, dsth);
|
||||
|
||||
- if (in_list) {
|
||||
- std::push_heap(pq.begin(), std::find(pq.begin(), pq.end(), static_cast<int>(index(loc))) + 1, node_comp);
|
||||
- } else {
|
||||
- pq.push_back(index(loc));
|
||||
- std::push_heap(pq.begin(), pq.end(), node_comp);
|
||||
- }
|
||||
+ // Always push a new entry. If the node was already in the
|
||||
+ // open set (in_list == true), the old entry becomes stale
|
||||
+ // and will be skipped when popped (lazy deletion).
|
||||
+ pq.push_back(index(loc));
|
||||
+ std::push_heap(pq.begin(), pq.end(), node_comp);
|
||||
}
|
||||
}
|
||||
BIN
defects/wesnoth-0001/test/wesnoth-0001-test
Executable file
BIN
defects/wesnoth-0001/test/wesnoth-0001-test
Executable file
Binary file not shown.
242
defects/wesnoth-0001/test/wesnoth-0001-test.cpp
Normal file
242
defects/wesnoth-0001/test/wesnoth-0001-test.cpp
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
// wesnoth-0001-test.cpp
|
||||
// Unit test: A* pathfinding std::find on priority queue vector (CWE-407)
|
||||
//
|
||||
// DEFECT: In astarsearch.cpp, the A* inner loop calls
|
||||
// std::push_heap(pq.begin(), std::find(pq.begin(), pq.end(), index), ...)
|
||||
// to perform decrease-key on a node already in the open set.
|
||||
// std::find scans the entire pq vector, making the per-relaxation cost O(Q)
|
||||
// where Q is the priority queue size. On hex maps with varied terrain costs,
|
||||
// the same hex gets relaxed multiple times, triggering the linear scan.
|
||||
//
|
||||
// FIX: Use lazy deletion. Always push a new entry on relaxation. When
|
||||
// popping, skip nodes that have already been finalized (in == search_counter).
|
||||
// This eliminates the std::find entirely.
|
||||
//
|
||||
// BUILD: g++ -std=c++17 -O2 -o wesnoth-0001-test wesnoth-0001-test.cpp && ./wesnoth-0001-test
|
||||
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <random>
|
||||
|
||||
static const unsigned SEARCH_COUNTER_BASE = 100;
|
||||
|
||||
struct Node {
|
||||
double g;
|
||||
double t;
|
||||
int prev;
|
||||
unsigned in;
|
||||
Node() : g(1e25), t(1e25), prev(-1), in(0) {}
|
||||
};
|
||||
|
||||
struct Comp {
|
||||
const std::vector<Node>& nodes;
|
||||
Comp(const std::vector<Node>& n) : nodes(n) {}
|
||||
bool operator()(int a, int b) const {
|
||||
return nodes[b].t < nodes[a].t;
|
||||
}
|
||||
};
|
||||
|
||||
// Dense random graph to force many decrease-key operations
|
||||
struct DenseGraph {
|
||||
int n; // number of nodes
|
||||
// adjacency: for each node, a list of (neighbor, cost) pairs
|
||||
std::vector<std::vector<std::pair<int,double>>> adj;
|
||||
|
||||
void init(int nodes, int avg_edges_per_node, unsigned seed) {
|
||||
n = nodes;
|
||||
adj.resize(n);
|
||||
std::mt19937 rng(seed);
|
||||
std::uniform_int_distribution<int> node_dist(0, n-1);
|
||||
std::uniform_real_distribution<double> cost_dist(1.0, 10.0);
|
||||
|
||||
// Create a connected graph with many edges to force decrease-keys
|
||||
// First create a chain
|
||||
for (int i = 0; i < n - 1; i++) {
|
||||
double c = cost_dist(rng);
|
||||
adj[i].push_back({i+1, c});
|
||||
adj[i+1].push_back({i, c});
|
||||
}
|
||||
// Then add random edges
|
||||
for (int i = 0; i < n * avg_edges_per_node; i++) {
|
||||
int u = node_dist(rng);
|
||||
int v = node_dist(rng);
|
||||
if (u != v) {
|
||||
double c = cost_dist(rng);
|
||||
adj[u].push_back({v, c});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// DEFECTIVE: uses std::find for decrease-key
|
||||
long long run_defective(const DenseGraph& g, int src, int dst, double* out_cost) {
|
||||
unsigned search_counter = SEARCH_COUNTER_BASE;
|
||||
std::vector<Node> nodes(g.n);
|
||||
Comp comp(nodes);
|
||||
|
||||
nodes[src].g = 0;
|
||||
nodes[src].t = 0;
|
||||
nodes[src].in = search_counter + 1;
|
||||
|
||||
std::vector<int> pq;
|
||||
pq.push_back(src);
|
||||
|
||||
long long ops = 0;
|
||||
long long decrease_keys = 0;
|
||||
|
||||
while (!pq.empty()) {
|
||||
int front_idx = pq.front();
|
||||
nodes[front_idx].in = search_counter;
|
||||
std::pop_heap(pq.begin(), pq.end(), comp);
|
||||
pq.pop_back();
|
||||
|
||||
Node& n = nodes[front_idx];
|
||||
if (n.g > 1e24) break;
|
||||
|
||||
for (auto& [nb, edge_cost] : g.adj[front_idx]) {
|
||||
double cost = n.g + edge_cost;
|
||||
if (nodes[nb].in == search_counter) continue;
|
||||
if (cost >= nodes[nb].g) continue;
|
||||
|
||||
bool in_list = nodes[nb].in == search_counter + 1;
|
||||
|
||||
nodes[nb].g = cost;
|
||||
nodes[nb].t = cost;
|
||||
nodes[nb].prev = front_idx;
|
||||
nodes[nb].in = search_counter + 1;
|
||||
|
||||
if (in_list) {
|
||||
decrease_keys++;
|
||||
// DEFECT: linear scan to find position in pq
|
||||
auto it = std::find(pq.begin(), pq.end(), nb);
|
||||
ops += (long long)(it - pq.begin()) + 1;
|
||||
if (it != pq.end()) {
|
||||
std::push_heap(pq.begin(), it + 1, comp);
|
||||
}
|
||||
} else {
|
||||
pq.push_back(nb);
|
||||
std::push_heap(pq.begin(), pq.end(), comp);
|
||||
ops++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*out_cost = nodes[dst].g;
|
||||
return ops;
|
||||
}
|
||||
|
||||
// FIXED: lazy deletion, no std::find
|
||||
long long run_fixed(const DenseGraph& g, int src, int dst, double* out_cost) {
|
||||
unsigned search_counter = SEARCH_COUNTER_BASE;
|
||||
std::vector<Node> nodes(g.n);
|
||||
Comp comp(nodes);
|
||||
|
||||
nodes[src].g = 0;
|
||||
nodes[src].t = 0;
|
||||
nodes[src].in = search_counter + 1;
|
||||
|
||||
std::vector<int> pq;
|
||||
pq.push_back(src);
|
||||
|
||||
long long ops = 0;
|
||||
|
||||
while (!pq.empty()) {
|
||||
std::pop_heap(pq.begin(), pq.end(), comp);
|
||||
int front_idx = pq.back();
|
||||
pq.pop_back();
|
||||
|
||||
Node& n = nodes[front_idx];
|
||||
|
||||
if (n.in == search_counter) continue;
|
||||
n.in = search_counter;
|
||||
|
||||
if (n.g > 1e24) break;
|
||||
|
||||
for (auto& [nb, edge_cost] : g.adj[front_idx]) {
|
||||
double cost = n.g + edge_cost;
|
||||
if (nodes[nb].in == search_counter) continue;
|
||||
if (cost >= nodes[nb].g) continue;
|
||||
|
||||
nodes[nb].g = cost;
|
||||
nodes[nb].t = cost;
|
||||
nodes[nb].prev = front_idx;
|
||||
nodes[nb].in = search_counter + 1;
|
||||
|
||||
pq.push_back(nb);
|
||||
std::push_heap(pq.begin(), pq.end(), comp);
|
||||
ops++;
|
||||
}
|
||||
}
|
||||
|
||||
*out_cost = nodes[dst].g;
|
||||
return ops;
|
||||
}
|
||||
|
||||
int main() {
|
||||
printf("wesnoth-0001-test: A* pathfinding std::find on pq (CWE-407)\n\n");
|
||||
|
||||
// Test 1: Correctness
|
||||
{
|
||||
printf("Test 1: correctness on small dense graph\n");
|
||||
DenseGraph g;
|
||||
g.init(100, 10, 42);
|
||||
|
||||
double cost_d, cost_f;
|
||||
run_defective(g, 0, 99, &cost_d);
|
||||
run_fixed(g, 0, 99, &cost_f);
|
||||
|
||||
assert(std::abs(cost_d - cost_f) < 1e-9);
|
||||
printf(" PASS (both find cost %.2f)\n", cost_d);
|
||||
}
|
||||
|
||||
// Test 2: Performance on medium graph
|
||||
{
|
||||
printf("\nTest 2: performance on dense graph (N=2000, E~20/node)\n");
|
||||
DenseGraph g;
|
||||
g.init(2000, 20, 123);
|
||||
|
||||
double cost_d, cost_f;
|
||||
long long ops_defect = run_defective(g, 0, 1999, &cost_d);
|
||||
long long ops_fixed = run_fixed(g, 0, 1999, &cost_f);
|
||||
|
||||
assert(std::abs(cost_d - cost_f) < 1e-9);
|
||||
|
||||
double ratio = (double)ops_defect / (double)ops_fixed;
|
||||
|
||||
printf(" defective ops: %lld\n", ops_defect);
|
||||
printf(" fixed ops: %lld\n", ops_fixed);
|
||||
printf(" ratio: %.1fx\n", ratio);
|
||||
printf(" cost: %.2f (both agree)\n", cost_d);
|
||||
assert(ratio > 5.0);
|
||||
printf(" PASS (ratio > 5x)\n");
|
||||
}
|
||||
|
||||
// Test 3: Performance on larger graph
|
||||
{
|
||||
printf("\nTest 3: performance on dense graph (N=5000, E~20/node)\n");
|
||||
DenseGraph g;
|
||||
g.init(5000, 20, 456);
|
||||
|
||||
double cost_d, cost_f;
|
||||
long long ops_defect = run_defective(g, 0, 4999, &cost_d);
|
||||
long long ops_fixed = run_fixed(g, 0, 4999, &cost_f);
|
||||
|
||||
assert(std::abs(cost_d - cost_f) < 1e-9);
|
||||
|
||||
double ratio = (double)ops_defect / (double)ops_fixed;
|
||||
|
||||
printf(" defective ops: %lld\n", ops_defect);
|
||||
printf(" fixed ops: %lld\n", ops_fixed);
|
||||
printf(" ratio: %.1fx\n", ratio);
|
||||
printf(" cost: %.2f (both agree)\n", cost_d);
|
||||
assert(ratio > 10.0);
|
||||
printf(" PASS (ratio > 10x)\n");
|
||||
}
|
||||
|
||||
printf("\nAll tests PASSED.\n");
|
||||
return 0;
|
||||
}
|
||||
63
defects/wesnoth-0002/patch/wesnoth-0002.patch
Normal file
63
defects/wesnoth-0002/patch/wesnoth-0002.patch
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# UNDF: UNDF-2026-000000971
|
||||
--- a/src/server/wesnothd/server.hpp
|
||||
+++ b/src/server/wesnothd/server.hpp
|
||||
@@ -111,6 +111,18 @@ private:
|
||||
struct connection_log
|
||||
{
|
||||
std::string nick, ip;
|
||||
+ std::chrono::system_clock::time_point log_off;
|
||||
+
|
||||
+ bool operator==(const connection_log& c) const
|
||||
+ {
|
||||
+ // log off time does not matter to find ip-nick pairs
|
||||
+ return c.nick == nick && c.ip == ip;
|
||||
+ }
|
||||
+ };
|
||||
+
|
||||
+ struct connection_log_hash
|
||||
+ {
|
||||
+ std::size_t operator()(const connection_log& c) const
|
||||
{
|
||||
+ std::size_t h1 = std::hash<std::string>{}(c.nick);
|
||||
+ std::size_t h2 = std::hash<std::string>{}(c.ip);
|
||||
+ return h1 ^ (h2 << 1);
|
||||
}
|
||||
};
|
||||
|
||||
- std::deque<connection_log> ip_log_;
|
||||
+ // Use an unordered_set for O(1) lookup instead of linear scan on deque.
|
||||
+ // Maintain a deque alongside for LRU eviction order.
|
||||
+ std::deque<connection_log> ip_log_;
|
||||
+ std::unordered_set<connection_log, connection_log_hash> ip_log_set_;
|
||||
|
||||
--- a/src/server/wesnothd/server.cpp
|
||||
+++ b/src/server/wesnothd/server.cpp
|
||||
@@ -896,7 +896,7 @@ void server::handle_new_client(socket_ptr socket)
|
||||
connection_log ip_name { username, client_address(socket), {} };
|
||||
|
||||
- if(std::find(ip_log_.begin(), ip_log_.end(), ip_name) == ip_log_.end()) {
|
||||
+ if(ip_log_set_.find(ip_name) == ip_log_set_.end()) {
|
||||
ip_log_.push_back(ip_name);
|
||||
+ ip_log_set_.insert(ip_name);
|
||||
|
||||
// Remove the oldest entry if the size of the IP log exceeds the maximum size
|
||||
if(ip_log_.size() > max_ip_log_size_) {
|
||||
+ ip_log_set_.erase(ip_log_.front());
|
||||
ip_log_.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2279,7 +2279,7 @@ void server::remove_player(player_iterator iter)
|
||||
connection_log ip_name { iter->info().name(), ip, {} };
|
||||
|
||||
- auto i = std::find(ip_log_.begin(), ip_log_.end(), ip_name);
|
||||
+ auto i = ip_log_set_.find(ip_name);
|
||||
- if(i != ip_log_.end()) {
|
||||
+ if(i != ip_log_set_.end()) {
|
||||
- i->log_off = std::chrono::system_clock::now();
|
||||
+ // Update log_off in the deque entry
|
||||
+ auto di = std::find(ip_log_.begin(), ip_log_.end(), ip_name);
|
||||
+ if(di != ip_log_.end()) {
|
||||
+ di->log_off = std::chrono::system_clock::now();
|
||||
+ }
|
||||
}
|
||||
BIN
defects/wesnoth-0002/test/wesnoth-0002-test
Executable file
BIN
defects/wesnoth-0002/test/wesnoth-0002-test
Executable file
Binary file not shown.
174
defects/wesnoth-0002/test/wesnoth-0002-test.cpp
Normal file
174
defects/wesnoth-0002/test/wesnoth-0002-test.cpp
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
// wesnoth-0002-test.cpp
|
||||
// Unit test: Server ip_log_ deque linear scan (CWE-407)
|
||||
//
|
||||
// DEFECT: In server.cpp, every player login checks ip_log_ (a deque of up to
|
||||
// 500 connection_log entries) via std::find for duplicate IP/nick pairs.
|
||||
// Every player logoff also does std::find to update log_off time.
|
||||
// Both are O(N) where N is the log size (default max 500).
|
||||
// Under heavy login/logoff traffic, this becomes O(L * N) where L is the
|
||||
// number of login events.
|
||||
//
|
||||
// FIX: Add an unordered_set<connection_log> alongside the deque for O(1)
|
||||
// membership lookup. Keep the deque for LRU eviction order.
|
||||
//
|
||||
// BUILD: g++ -std=c++17 -O2 -o wesnoth-0002-test wesnoth-0002-test.cpp && ./wesnoth-0002-test
|
||||
|
||||
#include <deque>
|
||||
#include <unordered_set>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cassert>
|
||||
|
||||
struct connection_log {
|
||||
std::string nick, ip;
|
||||
|
||||
bool operator==(const connection_log& c) const {
|
||||
return c.nick == nick && c.ip == ip;
|
||||
}
|
||||
};
|
||||
|
||||
struct connection_log_hash {
|
||||
std::size_t operator()(const connection_log& c) const {
|
||||
std::size_t h1 = std::hash<std::string>{}(c.nick);
|
||||
std::size_t h2 = std::hash<std::string>{}(c.ip);
|
||||
return h1 ^ (h2 << 1);
|
||||
}
|
||||
};
|
||||
|
||||
// DEFECTIVE: linear scan on deque
|
||||
struct DefectiveIpLog {
|
||||
std::deque<connection_log> ip_log_;
|
||||
std::size_t max_size_;
|
||||
|
||||
DefectiveIpLog(std::size_t max_sz) : max_size_(max_sz) {}
|
||||
|
||||
long long login(const std::string& nick, const std::string& ip) {
|
||||
connection_log entry{nick, ip};
|
||||
long long ops = 0;
|
||||
|
||||
// Linear scan
|
||||
auto it = ip_log_.begin();
|
||||
for (; it != ip_log_.end(); ++it) {
|
||||
ops++;
|
||||
if (*it == entry) break;
|
||||
}
|
||||
|
||||
if (it == ip_log_.end()) {
|
||||
ip_log_.push_back(entry);
|
||||
if (ip_log_.size() > max_size_) {
|
||||
ip_log_.pop_front();
|
||||
}
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
};
|
||||
|
||||
// FIXED: unordered_set for O(1) lookup
|
||||
struct FixedIpLog {
|
||||
std::deque<connection_log> ip_log_;
|
||||
std::unordered_set<connection_log, connection_log_hash> ip_log_set_;
|
||||
std::size_t max_size_;
|
||||
|
||||
FixedIpLog(std::size_t max_sz) : max_size_(max_sz) {}
|
||||
|
||||
long long login(const std::string& nick, const std::string& ip) {
|
||||
connection_log entry{nick, ip};
|
||||
long long ops = 1; // hash lookup = 1 op
|
||||
|
||||
if (ip_log_set_.find(entry) == ip_log_set_.end()) {
|
||||
ip_log_.push_back(entry);
|
||||
ip_log_set_.insert(entry);
|
||||
if (ip_log_.size() > max_size_) {
|
||||
ip_log_set_.erase(ip_log_.front());
|
||||
ip_log_.pop_front();
|
||||
}
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
};
|
||||
|
||||
int main() {
|
||||
printf("wesnoth-0002-test: server ip_log_ deque linear scan (CWE-407)\n\n");
|
||||
|
||||
const int MAX_LOG = 500;
|
||||
const int NUM_LOGINS = 2000;
|
||||
|
||||
// Test 1: Correctness
|
||||
{
|
||||
printf("Test 1: correctness\n");
|
||||
DefectiveIpLog defective(MAX_LOG);
|
||||
FixedIpLog fixed(MAX_LOG);
|
||||
|
||||
for (int i = 0; i < NUM_LOGINS; i++) {
|
||||
std::string nick = "user" + std::to_string(i % 600);
|
||||
std::string ip = "10.0." + std::to_string((i / 256) % 256) + "." + std::to_string(i % 256);
|
||||
defective.login(nick, ip);
|
||||
fixed.login(nick, ip);
|
||||
}
|
||||
|
||||
// Both should have same entries
|
||||
assert(defective.ip_log_.size() == fixed.ip_log_.size());
|
||||
for (size_t i = 0; i < defective.ip_log_.size(); i++) {
|
||||
assert(defective.ip_log_[i] == fixed.ip_log_[i]);
|
||||
}
|
||||
printf(" PASS\n");
|
||||
}
|
||||
|
||||
// Test 2: Performance
|
||||
{
|
||||
printf("\nTest 2: performance with %d logins, max_log=%d\n", NUM_LOGINS, MAX_LOG);
|
||||
|
||||
DefectiveIpLog defective(MAX_LOG);
|
||||
FixedIpLog fixed(MAX_LOG);
|
||||
|
||||
long long defect_ops = 0;
|
||||
long long fixed_ops = 0;
|
||||
|
||||
for (int i = 0; i < NUM_LOGINS; i++) {
|
||||
// Mix of new and repeat logins
|
||||
std::string nick = "player" + std::to_string(i % 700);
|
||||
std::string ip = "192.168." + std::to_string((i / 256) % 256) + "." + std::to_string(i % 256);
|
||||
defect_ops += defective.login(nick, ip);
|
||||
fixed_ops += fixed.login(nick, ip);
|
||||
}
|
||||
|
||||
double ratio = (double)defect_ops / (double)fixed_ops;
|
||||
|
||||
printf(" defective ops: %lld\n", defect_ops);
|
||||
printf(" fixed ops: %lld\n", fixed_ops);
|
||||
printf(" ratio: %.1fx\n", ratio);
|
||||
assert(ratio > 50.0);
|
||||
printf(" PASS (ratio > 50x)\n");
|
||||
}
|
||||
|
||||
// Test 3: Worst case (all unique logins filling the log)
|
||||
{
|
||||
printf("\nTest 3: worst case, all unique logins\n");
|
||||
|
||||
DefectiveIpLog defective(MAX_LOG);
|
||||
FixedIpLog fixed(MAX_LOG);
|
||||
|
||||
long long defect_ops = 0;
|
||||
long long fixed_ops = 0;
|
||||
|
||||
for (int i = 0; i < NUM_LOGINS; i++) {
|
||||
std::string nick = "unique_user_" + std::to_string(i);
|
||||
std::string ip = "10." + std::to_string((i / 65536) % 256) + "." + std::to_string((i / 256) % 256) + "." + std::to_string(i % 256);
|
||||
defect_ops += defective.login(nick, ip);
|
||||
fixed_ops += fixed.login(nick, ip);
|
||||
}
|
||||
|
||||
double ratio = (double)defect_ops / (double)fixed_ops;
|
||||
|
||||
printf(" defective ops: %lld\n", defect_ops);
|
||||
printf(" fixed ops: %lld\n", fixed_ops);
|
||||
printf(" ratio: %.1fx\n", ratio);
|
||||
assert(ratio > 100.0);
|
||||
printf(" PASS (ratio > 100x)\n");
|
||||
}
|
||||
|
||||
printf("\nAll tests PASSED.\n");
|
||||
return 0;
|
||||
}
|
||||
67
defects/wesnoth-0003/patch/wesnoth-0003.patch
Normal file
67
defects/wesnoth-0003/patch/wesnoth-0003.patch
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# UNDF: UNDF-2026-000000972
|
||||
--- a/src/units/types.cpp
|
||||
+++ b/src/units/types.cpp
|
||||
@@ -458,10 +458,11 @@ std::vector<t_string> unit_type::special_notes() const {
|
||||
|
||||
static void append_special_note(std::vector<t_string>& notes, const t_string& new_note) {
|
||||
if(new_note.empty()) return;
|
||||
std::string_view note_plain = new_note.c_str();
|
||||
utils::trim(note_plain);
|
||||
if(note_plain.empty()) return;
|
||||
- if(utils::contains(notes, new_note)) return;
|
||||
notes.push_back(new_note);
|
||||
}
|
||||
|
||||
-std::vector<t_string> combine_special_notes(const std::vector<t_string>& direct, const config& abilities, const const_attack_itors& attacks, const movetype& mt)
|
||||
+std::vector<t_string> combine_special_notes(const std::vector<t_string>& direct, const config& abilities, const const_attack_itors& attacks, const movetype& mt)
|
||||
{
|
||||
- std::vector<t_string> notes;
|
||||
+ std::vector<t_string> notes;
|
||||
+ // Use a set to track seen notes for O(1) dedup instead of
|
||||
+ // O(N) linear scan via utils::contains on vector per insertion.
|
||||
+ std::set<std::string> seen;
|
||||
for(const auto& note : direct) {
|
||||
- append_special_note(notes, note);
|
||||
+ std::string key(note.c_str());
|
||||
+ if(!key.empty() && seen.insert(key).second) {
|
||||
+ notes.push_back(note);
|
||||
+ }
|
||||
}
|
||||
for(const auto [key, cfg] : abilities.all_children_view()) {
|
||||
if(cfg.has_attribute("special_note")) {
|
||||
- append_special_note(notes, cfg["special_note"].t_str());
|
||||
+ const t_string& sn = cfg["special_note"].t_str();
|
||||
+ std::string k(sn.c_str());
|
||||
+ if(!k.empty() && seen.insert(k).second) {
|
||||
+ notes.push_back(sn);
|
||||
+ }
|
||||
}
|
||||
}
|
||||
for(const auto& attack : attacks) {
|
||||
for(const auto& p_ab : attack.specials()) {
|
||||
if(p_ab->cfg().has_attribute("special_note")) {
|
||||
- append_special_note(notes, p_ab->cfg()["special_note"].t_str());
|
||||
+ const t_string& sn = p_ab->cfg()["special_note"].t_str();
|
||||
+ std::string k(sn.c_str());
|
||||
+ if(!k.empty() && seen.insert(k).second) {
|
||||
+ notes.push_back(sn);
|
||||
+ }
|
||||
}
|
||||
}
|
||||
if(auto attack_type_note = string_table.find("special_note_damage_type_" + attack.type()); attack_type_note != string_table.end()) {
|
||||
- append_special_note(notes, attack_type_note->second);
|
||||
+ std::string k(attack_type_note->second.c_str());
|
||||
+ if(!k.empty() && seen.insert(k).second) {
|
||||
+ notes.push_back(attack_type_note->second);
|
||||
+ }
|
||||
}
|
||||
}
|
||||
for(const auto& move_note : mt.special_notes()) {
|
||||
- append_special_note(notes, move_note);
|
||||
+ std::string k(move_note.c_str());
|
||||
+ if(!k.empty() && seen.insert(k).second) {
|
||||
+ notes.push_back(move_note);
|
||||
+ }
|
||||
}
|
||||
return notes;
|
||||
}
|
||||
BIN
defects/wesnoth-0003/test/wesnoth-0003-test
Executable file
BIN
defects/wesnoth-0003/test/wesnoth-0003-test
Executable file
Binary file not shown.
141
defects/wesnoth-0003/test/wesnoth-0003-test.cpp
Normal file
141
defects/wesnoth-0003/test/wesnoth-0003-test.cpp
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
// wesnoth-0003-test.cpp
|
||||
// Unit test: combine_special_notes O(N^2) dedup (CWE-407)
|
||||
//
|
||||
// DEFECT: In types.cpp, combine_special_notes calls append_special_note for
|
||||
// every note from direct notes, abilities, attack specials, damage types,
|
||||
// and movement type. Each call does utils::contains(notes, new_note) which
|
||||
// is std::find on a vector, making the total cost O(N^2) where N is the
|
||||
// total number of note insertions.
|
||||
//
|
||||
// FIX: Use a std::set<std::string> to track seen notes for O(log N) dedup
|
||||
// (or unordered_set for O(1)). This reduces total cost to O(N log N).
|
||||
//
|
||||
// BUILD: g++ -std=c++17 -O2 -o wesnoth-0003-test wesnoth-0003-test.cpp && ./wesnoth-0003-test
|
||||
|
||||
#include <vector>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cassert>
|
||||
|
||||
// Simulate the defect: O(N) contains check per insertion
|
||||
long long defective_combine(const std::vector<std::string>& input) {
|
||||
std::vector<std::string> notes;
|
||||
long long ops = 0;
|
||||
|
||||
for (const auto& note : input) {
|
||||
if (note.empty()) continue;
|
||||
// Linear scan for dedup
|
||||
bool found = false;
|
||||
for (const auto& existing : notes) {
|
||||
ops++;
|
||||
if (existing == note) { found = true; break; }
|
||||
}
|
||||
if (!found) {
|
||||
notes.push_back(note);
|
||||
}
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
|
||||
// Fixed: set-based dedup
|
||||
long long fixed_combine(const std::vector<std::string>& input) {
|
||||
std::vector<std::string> notes;
|
||||
std::set<std::string> seen;
|
||||
long long ops = 0;
|
||||
|
||||
for (const auto& note : input) {
|
||||
if (note.empty()) continue;
|
||||
ops++; // set insertion/lookup
|
||||
if (seen.insert(note).second) {
|
||||
notes.push_back(note);
|
||||
}
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
|
||||
int main() {
|
||||
printf("wesnoth-0003-test: combine_special_notes O(N^2) dedup (CWE-407)\n\n");
|
||||
|
||||
// Test 1: Correctness
|
||||
{
|
||||
printf("Test 1: correctness\n");
|
||||
std::vector<std::string> input = {
|
||||
"Poison attack", "First strike", "Poison attack",
|
||||
"Regenerates", "First strike", "Skirmisher",
|
||||
"Regenerates", "Marksman", ""
|
||||
};
|
||||
|
||||
// Defective approach
|
||||
std::vector<std::string> defect_result;
|
||||
for (const auto& note : input) {
|
||||
if (note.empty()) continue;
|
||||
bool found = false;
|
||||
for (const auto& e : defect_result) {
|
||||
if (e == note) { found = true; break; }
|
||||
}
|
||||
if (!found) defect_result.push_back(note);
|
||||
}
|
||||
|
||||
// Fixed approach
|
||||
std::vector<std::string> fixed_result;
|
||||
std::set<std::string> seen;
|
||||
for (const auto& note : input) {
|
||||
if (note.empty()) continue;
|
||||
if (seen.insert(note).second) {
|
||||
fixed_result.push_back(note);
|
||||
}
|
||||
}
|
||||
|
||||
assert(defect_result == fixed_result);
|
||||
printf(" PASS (same output: %zu unique notes)\n", defect_result.size());
|
||||
}
|
||||
|
||||
// Test 2: Performance with many notes (simulating unit with many abilities)
|
||||
{
|
||||
const int N = 500;
|
||||
printf("\nTest 2: performance with N=%d notes (50%% duplicates)\n", N);
|
||||
|
||||
std::vector<std::string> input;
|
||||
for (int i = 0; i < N; i++) {
|
||||
input.push_back("special_note_" + std::to_string(i % (N / 2)));
|
||||
}
|
||||
|
||||
long long defect_ops = defective_combine(input);
|
||||
long long fixed_ops = fixed_combine(input);
|
||||
|
||||
double ratio = (double)defect_ops / (double)fixed_ops;
|
||||
|
||||
printf(" defective ops: %lld\n", defect_ops);
|
||||
printf(" fixed ops: %lld\n", fixed_ops);
|
||||
printf(" ratio: %.1fx\n", ratio);
|
||||
assert(ratio > 50.0);
|
||||
printf(" PASS (ratio > 50x)\n");
|
||||
}
|
||||
|
||||
// Test 3: All unique notes (worst case for defective)
|
||||
{
|
||||
const int N = 1000;
|
||||
printf("\nTest 3: worst case, N=%d all unique notes\n", N);
|
||||
|
||||
std::vector<std::string> input;
|
||||
for (int i = 0; i < N; i++) {
|
||||
input.push_back("unique_note_" + std::to_string(i));
|
||||
}
|
||||
|
||||
long long defect_ops = defective_combine(input);
|
||||
long long fixed_ops = fixed_combine(input);
|
||||
|
||||
double ratio = (double)defect_ops / (double)fixed_ops;
|
||||
|
||||
printf(" defective ops: %lld\n", defect_ops);
|
||||
printf(" fixed ops: %lld\n", fixed_ops);
|
||||
printf(" ratio: %.1fx\n", ratio);
|
||||
assert(ratio > 100.0);
|
||||
printf(" PASS (ratio > 100x)\n");
|
||||
}
|
||||
|
||||
printf("\nAll tests PASSED.\n");
|
||||
return 0;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue