java-topology/defects/openmw-0003/test/openmw-0003-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

118 lines
3.6 KiB
C++

// openmw-0003-test.cpp
// CWE-407: cellstore.cpp mMovedRefs std::find O(R*M)
//
// During cell loading, for each reference read from an ESM file, the code calls
// std::find(cell.mMovedRefs.begin(), cell.mMovedRefs.end(), ref.mRefNum) to check
// if the reference was moved to a different cell. mMovedRefs is a
// std::list<MovedCellRef>, so this is O(M) per reference. With R references and
// M moved refs, total cost is O(R*M).
//
// This pattern appears in cellstore.cpp (listRefs and loadRefs) and
// objectpaging.cpp. Cells can have hundreds of references and dozens of moved refs.
//
// Fix: build an unordered_set of moved ref numbers once before iterating, O(R+M).
#include <algorithm>
#include <cassert>
#include <chrono>
#include <cstdio>
#include <list>
#include <unordered_set>
#include <vector>
struct RefNum {
int mIndex;
int mContentFile;
bool operator==(const RefNum& other) const {
return mIndex == other.mIndex && mContentFile == other.mContentFile;
}
struct Hash {
size_t operator()(const RefNum& r) const {
return std::hash<long long>()(((long long)r.mContentFile << 32) | (unsigned)r.mIndex);
}
};
};
struct MovedCellRef {
RefNum mRefNum;
int mTarget[2];
bool operator==(const RefNum& ref) const {
return mRefNum == ref;
}
};
using MovedCellRefTracker = std::list<MovedCellRef>;
// ---------- DEFECTIVE: std::find on list per reference ----------
int loadRefsDefective(const std::vector<RefNum>& refs, const MovedCellRefTracker& movedRefs) {
int loaded = 0;
for (const auto& ref : refs) {
// O(M) linear scan per reference
auto iter = std::find(movedRefs.begin(), movedRefs.end(), ref);
if (iter != movedRefs.end())
continue;
loaded++;
}
return loaded;
}
// ---------- PATCHED: unordered_set pre-built once ----------
int loadRefsPatched(const std::vector<RefNum>& refs, const MovedCellRefTracker& movedRefs) {
std::unordered_set<RefNum, RefNum::Hash> movedRefSet;
movedRefSet.reserve(movedRefs.size());
for (const auto& moved : movedRefs)
movedRefSet.insert(moved.mRefNum);
int loaded = 0;
for (const auto& ref : refs) {
// O(1) hash set lookup
if (movedRefSet.count(ref))
continue;
loaded++;
}
return loaded;
}
int main() {
const int R = 5000; // references per cell
const int M = 200; // moved references
// Build test data
std::vector<RefNum> refs;
refs.reserve(R);
for (int i = 0; i < R; i++)
refs.push_back({i, 0});
MovedCellRefTracker movedRefs;
for (int i = 0; i < M; i++)
movedRefs.push_back({{i * 3, 0}, {0, 0}}); // every 3rd ref is moved
// Correctness
int r1 = loadRefsDefective(refs, movedRefs);
int r2 = loadRefsPatched(refs, movedRefs);
assert(r1 == r2);
printf("PASS correctness: both loaded %d refs (R=%d, M=%d)\n", r1, R, M);
// Performance
auto bench = [&](auto fn) {
auto t0 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 50; i++)
fn(refs, movedRefs);
auto t1 = std::chrono::high_resolution_clock::now();
return std::chrono::duration<double, std::milli>(t1 - t0).count() / 50.0;
};
double defMs = bench(loadRefsDefective);
double patMs = bench(loadRefsPatched);
double ratio = defMs / patMs;
printf("Defective: %.2f ms Patched: %.2f ms Ratio: %.1fx (R=%d, M=%d)\n",
defMs, patMs, ratio, R, M);
assert(ratio > 3.0 && "Patched should be at least 3x faster");
printf("PASS performance: %.1fx speedup\n", ratio);
return 0;
}