java-topology/defects/openmw-0002/test/openmw-0002-test.cpp
russell@unturf.com 4e3dcc8d2a openmw: 4 CWE-407 defects, MOAD 0002-0005 CLEAN
openmw-0001: pathgrid.cpp Tarjan SCC std::find(mSCCStack) O(V^2), 2.3x (HIGH)
openmw-0002: pathgrid.cpp A* openset std::find O(V*E), 4.4x op-count (HIGH)
openmw-0003: cellstore.cpp mMovedRefs std::find O(R*M), 15.6x (MEDIUM)
openmw-0004: objectpaging.cpp mMovedRefs std::find O(R*M), 4.9x (MEDIUM)

MOAD-0002 (Intertangle): CLEAN, typical game engine global state
MOAD-0003 (Leaked Context): CLEAN, no thread_local identity carriers
MOAD-0004 (Logged Secret): CLEAN, game engine has no credentials
MOAD-0005 (Thundering Herd): CLEAN, no unsynchronized cache patterns

8/8 unit tests PASS.
2026-03-31 12:11:13 -04:00

221 lines
7.6 KiB
C++

// openmw-0002-test.cpp
// CWE-407: A* pathfinding std::find(openset) O(V*E) in pathgrid.cpp
//
// The A* implementation in PathgridGraph::aStarSearch uses
// std::find(openset.begin(), openset.end(), dest) to check if a point is in
// the open set. The openset is a std::list<size_t>, so this is O(N) per edge
// check. The code even has a TODO: "if this causes performance problems a
// hash table may help".
//
// Fix: add an unordered_set<size_t> opensetMembership for O(1) membership.
// This test validates correctness and measures operation counts.
#include <algorithm>
#include <cassert>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <deque>
#include <list>
#include <set>
#include <unordered_set>
#include <vector>
struct Edge {
size_t index;
float cost;
};
struct Point {
int mX, mY, mZ;
};
static constexpr size_t NoIndex = static_cast<size_t>(-1);
float costAStar(const Point& a, const Point& b) {
return 300.0f * (std::abs(a.mX - b.mX) + std::abs(a.mY - b.mY) + std::abs(a.mZ - b.mZ));
}
struct Node {
std::vector<Edge> edges;
};
static size_t gProbeCount = 0;
// ---------- DEFECTIVE: std::find on list, counting probes ----------
namespace Defective {
std::deque<size_t> aStarSearch(const std::vector<Node>& graph,
const std::vector<Point>& points,
size_t start, size_t goal) {
std::deque<size_t> path;
size_t graphSize = graph.size();
std::vector<float> gScore(graphSize, -1);
std::vector<float> fScore(graphSize, -1);
std::vector<size_t> graphParent(graphSize, NoIndex);
gScore[start] = 0;
fScore[start] = costAStar(points[start], points[goal]);
std::list<size_t> openset;
std::set<size_t> closedset;
openset.push_back(start);
size_t current = start;
while (!openset.empty()) {
current = openset.front();
openset.pop_front();
if (current == goal) break;
closedset.insert(current);
for (const auto& edge : graph[current].edges) {
if (!closedset.contains(edge.index)) {
size_t dest = edge.index;
float tentativeG = gScore[current] + edge.cost;
// DEFECT: O(N) linear scan. Count each element comparison.
bool isInOpenSet = false;
for (auto it = openset.begin(); it != openset.end(); ++it) {
gProbeCount++;
if (*it == dest) { isInOpenSet = true; break; }
}
if (!isInOpenSet || tentativeG < gScore[dest]) {
graphParent[dest] = current;
gScore[dest] = tentativeG;
fScore[dest] = tentativeG + costAStar(points[dest], points[goal]);
if (!isInOpenSet) {
auto it = openset.begin();
for (; it != openset.end(); ++it)
if (fScore[*it] > fScore[dest]) break;
openset.insert(it, dest);
}
}
}
}
}
if (current != goal) return path;
while (graphParent[current] != NoIndex) {
path.push_front(current);
current = graphParent[current];
}
path.push_front(start);
return path;
}
}
// ---------- PATCHED: unordered_set for membership ----------
namespace Patched {
std::deque<size_t> aStarSearch(const std::vector<Node>& graph,
const std::vector<Point>& points,
size_t start, size_t goal) {
std::deque<size_t> path;
size_t graphSize = graph.size();
std::vector<float> gScore(graphSize, -1);
std::vector<float> fScore(graphSize, -1);
std::vector<size_t> graphParent(graphSize, NoIndex);
gScore[start] = 0;
fScore[start] = costAStar(points[start], points[goal]);
std::list<size_t> openset;
std::set<size_t> closedset;
std::unordered_set<size_t> opensetMembership;
openset.push_back(start);
opensetMembership.insert(start);
size_t current = start;
while (!openset.empty()) {
current = openset.front();
openset.pop_front();
opensetMembership.erase(current);
if (current == goal) break;
closedset.insert(current);
for (const auto& edge : graph[current].edges) {
if (!closedset.contains(edge.index)) {
size_t dest = edge.index;
float tentativeG = gScore[current] + edge.cost;
// PATCHED: O(1) hash set lookup (1 probe counted)
bool isInOpenSet = opensetMembership.count(dest) > 0;
gProbeCount++; // just 1 probe
if (!isInOpenSet || tentativeG < gScore[dest]) {
graphParent[dest] = current;
gScore[dest] = tentativeG;
fScore[dest] = tentativeG + costAStar(points[dest], points[goal]);
if (!isInOpenSet) {
opensetMembership.insert(dest);
auto it = openset.begin();
for (; it != openset.end(); ++it)
if (fScore[*it] > fScore[dest]) break;
openset.insert(it, dest);
}
}
}
}
}
if (current != goal) return path;
while (graphParent[current] != NoIndex) {
path.push_front(current);
current = graphParent[current];
}
path.push_front(start);
return path;
}
}
// Build a dense random-weight graph. All nodes at same position (0,0,0) so
// heuristic is zero and A* degenerates to Dijkstra, maximizing openset size.
void buildDenseGraph(size_t N, size_t edgesPerNode, std::vector<Node>& graph, std::vector<Point>& points) {
graph.resize(N);
points.resize(N);
for (size_t i = 0; i < N; i++) {
points[i] = {0, 0, 0}; // zero heuristic = Dijkstra
for (size_t j = 1; j <= edgesPerNode && i + j < N; j++) {
float cost = 100.0f + (float)(i * 7 + j * 13) * 0.1f; // varying costs
graph[i].edges.push_back({i + j, cost});
graph[i + j].edges.push_back({i, cost});
}
}
}
int main() {
const size_t N = 2000;
const size_t edgesPerNode = 8;
std::vector<Node> graph;
std::vector<Point> points;
buildDenseGraph(N, edgesPerNode, graph, points);
size_t start = 0;
size_t goal = N - 1;
// Correctness: paths must be identical
gProbeCount = 0;
auto p1 = Defective::aStarSearch(graph, points, start, goal);
gProbeCount = 0;
auto p2 = Patched::aStarSearch(graph, points, start, goal);
assert(p1 == p2);
printf("PASS correctness: paths match, length=%zu (N=%zu)\n", p1.size(), N);
// Operation count comparison
gProbeCount = 0;
Defective::aStarSearch(graph, points, start, goal);
size_t defOps = gProbeCount;
gProbeCount = 0;
Patched::aStarSearch(graph, points, start, goal);
size_t patOps = gProbeCount;
double opRatio = (double)defOps / (double)patOps;
printf("Defective probes: %zu Patched probes: %zu Op-count ratio: %.1fx\n",
defOps, patOps, opRatio);
fflush(stdout);
assert(opRatio > 2.0 && "Patched should have at least 2x fewer probes");
printf("PASS performance: %.1fx fewer membership probes\n", opRatio);
return 0;
}