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

122 lines
3.9 KiB
C++

// openmw-0004-test.cpp
// CWE-407: objectpaging.cpp mMovedRefs std::find O(R*M) per cell during paging
//
// In ObjectPaging::createChunk, for every reference read from a cell ESM file,
// std::find(cell->mMovedRefs.begin(), cell->mMovedRefs.end(), ref.mRefNum) is
// called. This is the same mMovedRefs linear-scan pattern as openmw-0003, but
// in the object paging codepath which processes many cells during terrain loading.
//
// Fix: build unordered_set of moved ref numbers once per cell, O(1) lookups.
#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;
bool operator==(const RefNum& ref) const {
return mRefNum == ref;
}
};
using MovedCellRefTracker = std::list<MovedCellRef>;
struct Cell {
MovedCellRefTracker mMovedRefs;
};
// Simulate processing refs from multiple cells (object paging processes many cells)
// ---------- DEFECTIVE ----------
int processPageDefective(const std::vector<Cell>& cells, const std::vector<std::vector<RefNum>>& cellRefs) {
int processed = 0;
for (size_t c = 0; c < cells.size(); c++) {
for (const auto& ref : cellRefs[c]) {
// O(M) per ref per cell
if (std::find(cells[c].mMovedRefs.begin(), cells[c].mMovedRefs.end(), ref)
!= cells[c].mMovedRefs.end())
continue;
processed++;
}
}
return processed;
}
// ---------- PATCHED ----------
int processPagePatched(const std::vector<Cell>& cells, const std::vector<std::vector<RefNum>>& cellRefs) {
int processed = 0;
for (size_t c = 0; c < cells.size(); c++) {
// Build set once per cell
std::unordered_set<RefNum, RefNum::Hash> movedSet;
movedSet.reserve(cells[c].mMovedRefs.size());
for (const auto& moved : cells[c].mMovedRefs)
movedSet.insert(moved.mRefNum);
for (const auto& ref : cellRefs[c]) {
if (movedSet.count(ref))
continue;
processed++;
}
}
return processed;
}
int main() {
const int numCells = 25; // typical paging chunk processes ~25 cells
const int refsPerCell = 500;
const int movedPerCell = 100;
std::vector<Cell> cells(numCells);
std::vector<std::vector<RefNum>> cellRefs(numCells);
for (int c = 0; c < numCells; c++) {
for (int i = 0; i < refsPerCell; i++)
cellRefs[c].push_back({i, c});
for (int i = 0; i < movedPerCell; i++)
cells[c].mMovedRefs.push_back({{i * 4, c}});
}
// Correctness
int r1 = processPageDefective(cells, cellRefs);
int r2 = processPagePatched(cells, cellRefs);
assert(r1 == r2);
printf("PASS correctness: both processed %d refs (%d cells x %d refs)\n",
r1, numCells, refsPerCell);
// Performance
auto bench = [&](auto fn) {
auto t0 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 100; i++)
fn(cells, cellRefs);
auto t1 = std::chrono::high_resolution_clock::now();
return std::chrono::duration<double, std::milli>(t1 - t0).count() / 100.0;
};
double defMs = bench(processPageDefective);
double patMs = bench(processPagePatched);
double ratio = defMs / patMs;
printf("Defective: %.2f ms Patched: %.2f ms Ratio: %.1fx\n", defMs, patMs, ratio);
assert(ratio > 2.0 && "Patched should be at least 2x faster");
printf("PASS performance: %.1fx speedup\n", ratio);
return 0;
}