0ad (4), aranym, ardour, argo-cd, aria2, azahar, bcoin, bind9, btcpayserver (3), bullet3. Mix of CWE-407 and CWE-312.
3.2 KiB
0 A.D. — CWE-407 Disclosure Brief (0ad-0001)
2026-04-13 · Patch available — awaiting upstream merge
Finding
One O(n²) defect in 0 A.D.'s obstruction manager. The dirty shape tracking system uses std::vector with std::find for deduplication, causing O(N*D) per-frame cost where N = nearby shapes and D = dirty list size. In large battles (200v200), hundreds of shapes move per frame, making this quadratic.
The Defect
0ad-0001 (PATCHED — HIGH): source/simulation2/components/CCmpObstructionManager.cpp:525
// In CCmpObstructionManager — dirty shape dedup fires per frame:
std::vector<u32> m_DirtyStaticShapes;
std::vector<u32> m_DirtyUnitShapes;
// Every MakeDirtyStatic/MakeDirtyUnit call does:
if (std::find(m_DirtyStaticShapes.begin(), m_DirtyStaticShapes.end(), index)
== m_DirtyStaticShapes.end())
m_DirtyStaticShapes.push_back(index);
m_DirtyStaticShapes and m_DirtyUnitShapes accumulate during each frame. Every shape movement triggers MakeDirtyStatic or MakeDirtyUnit, which scans the growing vector with std::find before appending. The RasterizeHelper step also does per-shape membership checks against these vectors. In large battles with hundreds of moving units, the dirty lists grow large and every insertion plus every rasterize check pays O(D).
Complexity Proof
At D=400 dirty shapes per frame (200v200 battle):
- Defective: 400 insertions × average 200 comparisons = ~80,000 comparisons per frame, plus rasterize checks
- Fixed: 400 insertions × O(1) = 400 hash lookups per frame
- ~200× op reduction per frame. Fires every simulation frame.
Impact
0 A.D. is a popular open-source real-time strategy game with large-scale battles as a core gameplay feature. The defect fires every simulation frame during combat. Players running large battles (200v200 or larger) experience frame drops as the dirty shape dedup cost grows quadratically with battle size.
The Fix
Replace std::vector<u32> with std::unordered_set<u32> for both dirty shape lists:
// Before
std::vector<u32> m_DirtyStaticShapes;
std::vector<u32> m_DirtyUnitShapes;
if (std::find(m_DirtyStaticShapes.begin(), m_DirtyStaticShapes.end(), index)
== m_DirtyStaticShapes.end())
m_DirtyStaticShapes.push_back(index);
// After
// CWE-407 fix: unordered_set for O(1) membership test instead of O(D) vector scan.
std::unordered_set<u32> m_DirtyStaticShapes;
std::unordered_set<u32> m_DirtyUnitShapes;
m_DirtyStaticShapes.insert(index);
Patch
Fix available: defects/0ad-0001/patch/0ad-0001.patch
Single-file patch across CCmpObstructionManager.cpp. Replaces all std::find + push_back patterns with std::unordered_set::insert, and updates rasterize membership checks to use set::find.
What We Ask
A patch is ready for review.
- Confirm receipt and assign an issue reference (0ad/0ad or Wildfire Games Trac).
- Assess severity — this defect fires every simulation frame during battles.
- Coordinate a disclosure date — we target 90 days from first contact.
- We will credit the 0 A.D. team in the public disclosure. Preferred acknowledgment format welcome.
Contact: see cover email. This brief is confidential until coordinated disclosure.