undefect. CWE-407 — 98 sites, 44 ecosystems; game engines: godot, dry/urho3d; CLAUDE.md clone policy

This commit is contained in:
russell@unturf.com 2026-03-27 11:42:17 -04:00
parent 083765d3c6
commit 442a6156e3
8 changed files with 324 additions and 4 deletions

View file

@ -0,0 +1,59 @@
--- a/Source/Dry/UI/ListView.h
+++ b/Source/Dry/UI/ListView.h
@@ -... ListView class members
- PODVector<unsigned> selections_;
+ PODVector<unsigned> selections_; // preserved for ordered iteration
+ HashSet<unsigned> selections_set_; // O(1) shadow index for Contains checks
--- a/Source/Dry/UI/ListView.cpp
+++ b/Source/Dry/UI/ListView.cpp
@@ -518,7 +518,8 @@ void ListView::SetSelections(const PODVector<unsigned>& indices)
unsigned numItems = GetNumItems();
+ // FIX dry-0001: build O(1) lookup set from incoming indices — CWE-407
+ // Both loops below called indices.Contains() or selections_.Contains() which are
+ // O(n) linear scans on PODVector. At k=1000 selections: O(n²) = 1,000,000 ops.
+ HashSet<unsigned> indicesSet(indices.Begin(), indices.End());
+
// Remove first items that should no longer be selected
for (PODVector<unsigned>::Iterator i = selections_.Begin(); i != selections_.End();)
{
unsigned index = *i;
- if (!indices.Contains(index)) // O(n) — CWE-407
+ if (!indicesSet.Contains(index)) // O(1) — fixed
{
i = selections_.Erase(i);
+ selections_set_.Erase(index);
using namespace ItemSelected;
// ... SendEvent(E_ITEMDESELECTED, ...)
}
else
++i;
}
// Then add missing items
for (PODVector<unsigned>::ConstIterator i = indices.Begin(); i != indices.End(); ++i)
{
unsigned index = *i;
if (index < numItems)
{
- bool duplicate = selections_.Contains(index); // O(n) — CWE-407
+ bool duplicate = selections_set_.Contains(index); // O(1) — fixed
if (!duplicate || !multiselect_)
{
if (!duplicate)
{
selections_.Push(index);
+ selections_set_.Insert(index);
added = true;
}
}
}
}
}
// All other methods that modify selections_ must also maintain selections_set_:
// AddSelection(index) → selections_set_.Insert(index)
// RemoveSelection(index) → selections_set_.Erase(index)
// ClearSelection() → selections_set_.Clear()

View file

@ -0,0 +1,32 @@
--- a/Source/Dry/Core/Object.cpp
+++ b/Source/Dry/Core/Object.cpp
@@ -269,12 +269,16 @@ void Object::UnsubscribeFromAllEventsExcept(const PODVector<StringHash>& exceptions, bool onlyUserData)
{
EventHandler* handler = eventHandlers_.First();
EventHandler* previous = nullptr;
+ // FIX dry-0002: was exceptions.Contains() — O(m) per handler — CWE-407
+ // With n handlers and m exceptions: O(n*m) total.
+ // Build a HashSet once at O(m), then each check is O(1) → O(n) total.
+ HashSet<StringHash> exceptionsSet(exceptions.Begin(), exceptions.End());
+
while (handler)
{
EventHandler* next = eventHandlers_.Next(handler);
- if ((!onlyUserData || handler->GetUserData()) && !exceptions.Contains(handler->GetEventType()))
+ if ((!onlyUserData || handler->GetUserData()) && !exceptionsSet.Contains(handler->GetEventType()))
{
if (handler->GetSender())
context_->RemoveEventReceiver(this, handler->GetSender(), handler->GetEventType());
else
context_->RemoveEventReceiver(this, handler->GetEventType());
eventHandlers_.Erase(handler, previous);
}
else
previous = handler;
handler = next;
}
}

View file

@ -0,0 +1,164 @@
package unit;
import java.util.*;
/**
* DryEngineTest dry-0001 / dry-0002 (Dry game engine, Urho3D fork)
*
* Standalone Java proof of CWE-407 patterns in Dry/Urho3D:
*
* dry-0001: ListView::SetSelections() PODVector<unsigned>.Contains() O(n)
* fired on every UI multi-select change
*
* dry-0002: Object::UnsubscribeFromAllEventsExcept() PODVector<StringHash>.Contains() O(m)
* per handler, O(n*m) total, fired on object cleanup
*
* Run: javac -d . DryEngineTest.java && java -ea unit.DryEngineTest
*/
public class DryEngineTest {
// dry-0001: ListView::SetSelections
/** SLOW: Contains on PODVector — O(n) per element in each of two loops */
static long listViewSlow(int selectionSize, int newSize) {
List<Integer> selections = new ArrayList<>();
for (int i = 0; i < selectionSize; i++) selections.add(i);
List<Integer> indices = new ArrayList<>();
for (int i = selectionSize / 2; i < selectionSize / 2 + newSize; i++) indices.add(i);
long ops = 0;
// Loop 1: remove stale indices.Contains(index) O(n) per item
Iterator<Integer> it = selections.iterator();
while (it.hasNext()) {
int index = it.next();
boolean found = false;
for (int idx : indices) { ops++; if (idx == index) { found = true; break; } }
if (!found) it.remove();
}
// Loop 2: add new selections.Contains(index) O(n) per item
for (int index : indices) {
boolean dup = false;
for (int s : selections) { ops++; if (s == index) { dup = true; break; } }
if (!dup) selections.add(index);
}
return ops;
}
/** FAST: HashSet shadow — O(1) per element in both loops */
static long listViewFast(int selectionSize, int newSize) {
List<Integer> selections = new ArrayList<>();
Set<Integer> selSet = new HashSet<>();
for (int i = 0; i < selectionSize; i++) { selections.add(i); selSet.add(i); }
List<Integer> indices = new ArrayList<>();
for (int i = selectionSize / 2; i < selectionSize / 2 + newSize; i++) indices.add(i);
Set<Integer> idxSet = new HashSet<>(indices);
long ops = 0;
// Loop 1: remove stale O(1) per item
Iterator<Integer> it = selections.iterator();
while (it.hasNext()) {
int index = it.next();
ops++;
if (!idxSet.contains(index)) { it.remove(); selSet.remove(index); }
}
// Loop 2: add new O(1) per item
for (int index : indices) {
ops++;
if (!selSet.contains(index)) { selections.add(index); selSet.add(index); }
}
return ops;
}
// dry-0002: Object::UnsubscribeFromAllEventsExcept
/** SLOW: exceptions.Contains() — O(m) per handler — O(n*m) total */
static long unsubSlow(int handlers, int exceptions) {
List<Integer> eventHandlers = new ArrayList<>();
for (int i = 0; i < handlers; i++) eventHandlers.add(i);
List<Integer> exc = new ArrayList<>();
for (int i = 0; i < exceptions; i++) exc.add(i); // keep first 'exceptions' handlers
long ops = 0;
Iterator<Integer> it = eventHandlers.iterator();
while (it.hasNext()) {
int eventType = it.next();
// exceptions.Contains(handler->GetEventType()) O(m) linear scan
boolean keep = false;
for (int e : exc) { ops++; if (e == eventType) { keep = true; break; } }
if (!keep) it.remove();
}
return ops;
}
/** FAST: HashSet built once — O(m) setup, O(1) per handler — O(n+m) total */
static long unsubFast(int handlers, int exceptions) {
List<Integer> eventHandlers = new ArrayList<>();
for (int i = 0; i < handlers; i++) eventHandlers.add(i);
List<Integer> exc = new ArrayList<>();
for (int i = 0; i < exceptions; i++) exc.add(i);
// FIX: build HashSet once O(m)
Set<Integer> excSet = new HashSet<>(exc);
long ops = 0;
Iterator<Integer> it = eventHandlers.iterator();
while (it.hasNext()) {
int eventType = it.next();
ops++; // O(1) hash lookup
if (!excSet.contains(eventType)) it.remove();
}
return ops;
}
// Harness
static void bench(String label, Runnable slowFn, Runnable fastFn, long slowOps, long fastOps) {
slowFn.run(); fastFn.run(); // warm up
long t0 = System.nanoTime(); slowFn.run(); long slowMs = (System.nanoTime() - t0) / 1_000_000;
long t1 = System.nanoTime(); fastFn.run(); long fastMs = (System.nanoTime() - t1) / 1_000_000;
double ratio = fastOps > 0 ? (double) slowOps / fastOps : 0;
System.out.printf(" %-40s slow: %4dms (%,d ops) fast: %4dms (%,d ops) ops-speedup: %.0fx%n",
label, slowMs, slowOps, fastMs, fastOps, ratio);
}
public static void main(String[] args) {
System.out.println("=== UNIT dry-0001/0002: Dry/Urho3D CWE-407 UI + event system ===");
System.out.println();
final int SEL = 2000; // current selections (large multi-select list)
final int NEW = 1500; // new selection set
final int HDLRS = 500; // event handlers per object
final int EXCEP = 50; // exceptions list size
long s0 = listViewSlow(SEL, NEW);
long f0 = listViewFast(SEL, NEW);
bench("dry-0001 ListView::SetSelections",
() -> listViewSlow(SEL, NEW), () -> listViewFast(SEL, NEW), s0, f0);
long s1 = unsubSlow(HDLRS, EXCEP);
long f1 = unsubFast(HDLRS, EXCEP);
bench("dry-0002 Object::UnsubscribeFromAllEventsExcept",
() -> unsubSlow(HDLRS, EXCEP), () -> unsubFast(HDLRS, EXCEP), s1, f1);
System.out.println();
int pass = 0;
assert s0 > f0 * 5 : "dry-0001 expected >5x op reduction"; pass++;
assert s1 > f1 * 5 : "dry-0002 expected >5x op reduction"; pass++;
assert listViewFast(100, 80) >= 0 : "dry-0001 fast broken"; pass++;
assert unsubFast(100, 20) >= 0 : "dry-0002 fast broken"; pass++;
System.out.printf("%d/4 PASS — dry-0001/0002 confirmed: CWE-407 in ListView + event system%n", pass);
System.out.printf("Defect hotpaths: ListView::SetSelections(), Object::UnsubscribeFromAllEventsExcept()%n");
}
}