diff --git a/defects/allegro5/patch/CLEAN.md b/defects/allegro5/patch/CLEAN.md new file mode 100644 index 000000000..a01d90206 --- /dev/null +++ b/defects/allegro5/patch/CLEAN.md @@ -0,0 +1,24 @@ +# allegro5 — CWE-407 Scan Result: CLEAN + +**Scanned:** 2026-03-29 +**Repo:** https://github.com/liballeg/allegro5 + +## Findings + +All `_al_vector_contains` / `_al_list_contains` usages in allegro5 are on +non-hot paths: + +| Site | Path | Hot? | +|------|------|------| +| `al_register_event_source` | `src/events.c:168` | No — called once at setup | +| `al_is_event_source_registered` | `src/events.c:153` | No — query, not per-event | +| `al_destroy_shader` (bitmap dedup) | `src/shader.c:242` | No — called at teardown | +| `wwindow.c resizing_displays` | `src/win/wwindow.c:975` | No — OS resize callback | + +Font fallback chain (`addons/ttf/ttf.c`, `addons/font/font.c`) uses a singly-linked +pointer chain (`f->fallback`), not a list membership scan. + +No O(N²) or O(N×M) inner-loop membership checks found in any hot path +(per-frame, per-event, per-pixel). + +**Verdict: CLEAN** diff --git a/defects/box2d/patch/CLEAN.md b/defects/box2d/patch/CLEAN.md new file mode 100644 index 000000000..d42018b22 --- /dev/null +++ b/defects/box2d/patch/CLEAN.md @@ -0,0 +1,17 @@ +# box2d — CWE-407 Scan Result: CLEAN + +**Scanned:** 2026-03-29 +**Repo:** https://github.com/erincatto/box2d (v3.x C rewrite) + +## Findings + +Box2D v3 is a complete rewrite in C. All membership / deduplication structures +use `b2HashSet` (open-addressing hash table defined in `src/table.h`): + +- Broad-phase pair dedup: `b2HashSet` with `B2_SHAPE_PAIR_KEY(K1, K2)` — O(1) +- Island body/contact/joint tracking: array with direct ID indexing — O(1) +- Sensor overlap tracking: hash-set — O(1) + +No `std::find`, no linear scan dedup loops found in any hot path. + +**Verdict: CLEAN** diff --git a/defects/bullet3/patch/bullet3-0001-ghost-object-overlapping-hashset.md b/defects/bullet3/patch/bullet3-0001-ghost-object-overlapping-hashset.md new file mode 100644 index 000000000..67db61cf0 --- /dev/null +++ b/defects/bullet3/patch/bullet3-0001-ghost-object-overlapping-hashset.md @@ -0,0 +1,143 @@ +# UNDF: UNDF-2026-000000001 +# UNDF: (pending) +# bullet3-0001: btGhostObject::addOverlappingObjectInternal — O(N²) linear dedup + +## CWE-407 — Algorithmic Complexity + +| Field | Value | +|-------|-------| +| ID | bullet3-0001 | +| Severity | HIGH | +| Ecosystem | bullet3 | +| Package | BulletCollision/CollisionDispatch | +| File | `src/BulletCollision/CollisionDispatch/btGhostObject.cpp` | +| Lines | 37–42, 49–54, 75–80, 90–95 | +| Complexity | O(N²) total per broadphase step | +| Hot path | per broadphase overlap pair update (every physics step) | + +## Background + +`btGhostObject` is used for trigger volumes and character controllers +(`btKinematicCharacterController` uses `btPairCachingGhostObject`). +Every physics step the broadphase calls `addOverlappingObjectInternal` and +`removeOverlappingObjectInternal` for each new/removed pair that involves the +ghost object. + +## Defect + +`m_overlappingObjects` is a `btAlignedObjectArray` (a plain +dynamic array). Both add and remove call `findLinearSearch` which scans the +entire array to find duplicates or locate the element to remove. + +```cpp +// btGhostObject.cpp line 37 — the code itself contains the admission: +///if this linearSearch becomes too slow (too many overlapping objects) we should add +///a more appropriate data structure +int index = m_overlappingObjects.findLinearSearch(otherObject); +if (index == m_overlappingObjects.size()) +{ + //not found + m_overlappingObjects.push_back(otherObject); +} +``` + +The same pattern occurs in `btPairCachingGhostObject::addOverlappingObjectInternal` +(line 75) and both `removeOverlappingObjectInternal` variants (lines 49, 90). + +With N objects overlapping the ghost, each broadphase update requires O(N) work +per new pair. In a dense scene where G ghost objects each overlap N bodies, the +total cost per step is O(G × N²) in the worst case (N re-insertions checked +against an N-element list). + +## Fix + +Replace `m_overlappingObjects` with a `btHashSet`-backed structure. Since +`btCollisionObject*` pointers are unique, pointer value works as the hash key. + +**Option A — drop-in O(1) membership check using `btHashMap` as a set:** + +```cpp +// btGhostObject.h — add alongside m_overlappingObjects +#include "LinearMath/btHashMap.h" + +btAlignedObjectArray m_overlappingObjects; +btHashMap m_overlappingIndex; // ptr → index in array + +// addOverlappingObjectInternal +void btGhostObject::addOverlappingObjectInternal(btBroadphaseProxy* otherProxy, + btBroadphaseProxy* thisProxy) +{ + btCollisionObject* otherObject = (btCollisionObject*)otherProxy->m_clientObject; + btAssert(otherObject); + btHashPtr key(otherObject); + if (m_overlappingIndex.find(key) == nullptr) + { + int idx = m_overlappingObjects.size(); + m_overlappingObjects.push_back(otherObject); + m_overlappingIndex.insert(key, idx); + } +} + +// removeOverlappingObjectInternal +void btGhostObject::removeOverlappingObjectInternal(btBroadphaseProxy* otherProxy, + btDispatcher* dispatcher, + btBroadphaseProxy* thisProxy) +{ + btCollisionObject* otherObject = (btCollisionObject*)otherProxy->m_clientObject; + btAssert(otherObject); + btHashPtr key(otherObject); + const int* pIdx = m_overlappingIndex.find(key); + if (pIdx) + { + int index = *pIdx; + // swap-erase: move last element into freed slot + int lastIdx = m_overlappingObjects.size() - 1; + if (index != lastIdx) + { + btCollisionObject* last = m_overlappingObjects[lastIdx]; + m_overlappingObjects[index] = last; + m_overlappingIndex.insert(btHashPtr(last), index); + } + m_overlappingObjects.pop_back(); + m_overlappingIndex.remove(key); + } +} +``` + +**Option B (minimal change) — keep existing array, guard entry using `btHashSet`:** + +```cpp +// Use btHashMap as a membership guard alongside the existing +// array to preserve the indexed getOverlappingObject(int) API. +``` + +The library already provides `btHashedOverlappingPairCache` as a hash-based +alternative to `btSortedOverlappingPairCache`, so the pattern is established. + +## Speedup + +| N (overlapping objects per ghost) | Before (ops) | After (ops) | Speedup | +|-----------------------------------|--------------|-------------|---------| +| 10 | 100 | 10 | 10× | +| 50 | 2,500 | 50 | 50× | +| 100 | 10,000 | 100 | 100× | +| 500 | 250,000 | 500 | 500× | + +Character controllers with large trigger volumes (area-of-effect spells, +AI perception spheres) in complex scenes are the primary beneficiaries. + +## Note + +`b3SortedOverlappingPairCache::findPair` in +`src/Bullet3Collision/BroadPhaseCollision/b3OverlappingPairCache.cpp` carries +the same pattern with an even more explicit comment: + +```cpp +///this findPair becomes really slow. Either sort the list to speedup the query, +///or use a different solution. +b3BroadphasePair* b3SortedOverlappingPairCache::findPair(int proxy0, int proxy1) +{ + int findIndex = m_overlappingPairArray.findLinearSearch(tmpPair); +``` + +This is the Bullet3 (GPU) broadphase equivalent of the same defect. diff --git a/defects/bullet3/patch/bullet3-0002-soft-body-collision-disabled-hashset.md b/defects/bullet3/patch/bullet3-0002-soft-body-collision-disabled-hashset.md new file mode 100644 index 000000000..e9d5cf177 --- /dev/null +++ b/defects/bullet3/patch/bullet3-0002-soft-body-collision-disabled-hashset.md @@ -0,0 +1,97 @@ +# UNDF: (pending) +# bullet3-0002: btSoftRigidCollisionAlgorithm::processCollision — O(C×D) per frame linear scan + +## CWE-407 — Algorithmic Complexity + +| Field | Value | +|-------|-------| +| ID | bullet3-0002 | +| Severity | MEDIUM | +| Ecosystem | bullet3 | +| Package | BulletSoftBody | +| File | `src/BulletSoftBody/btSoftRigidCollisionAlgorithm.cpp:63`, `src/BulletSoftBody/btSoftBody.cpp:515` | +| Lines | 63 (algorithm); 515 (appendAnchor) | +| Complexity | O(C×D) per physics step where C = collision pairs, D = disabled objects | +| Hot path | per-collision-pair per physics step | + +## Defect + +`btSoftBody::m_collisionDisabledObjects` is a +`btAlignedObjectArray` (plain array). The soft-body +collision algorithm calls `findLinearSearch` on this array every time it +processes a collision pair between a soft body and a rigid body: + +```cpp +// btSoftRigidCollisionAlgorithm.cpp:63 — called every physics step per pair +void btSoftRigidCollisionAlgorithm::processCollision(...) +{ + BT_PROFILE("btSoftRigidCollisionAlgorithm::processCollision"); + btSoftBody* softBody = ...; + + if (softBody->m_collisionDisabledObjects.findLinearSearch( + rigidCollisionObjectWrap->getCollisionObject()) + == softBody->m_collisionDisabledObjects.size()) + { + softBody->getSoftBodySolver()->processCollision(softBody, rigidCollisionObjectWrap); + } +} +``` + +`m_collisionDisabledObjects` grows when anchors are added with +`disableCollisionBetweenLinkedBodies = true`: + +```cpp +// btSoftBody.cpp:515 +void btSoftBody::appendAnchor(int node, btRigidBody* body, const btVector3& localPivot, + bool disableCollisionBetweenLinkedBodies, btScalar influence) +{ + if (disableCollisionBetweenLinkedBodies) + { + if (m_collisionDisabledObjects.findLinearSearch(body) == m_collisionDisabledObjects.size()) + m_collisionDisabledObjects.push_back(body); + } +``` + +A cloth with A anchors to different rigid bodies has D ≤ A disabled objects. +Every physics step, for each of C rigid bodies that overlap the soft body +AABB, a linear scan of D objects is performed → O(C × D) per step. +With a richly-anchored cloth (D=50) in a complex scene (C=100 overlapping +rigid bodies), that is 5,000 operations per step at 60 Hz. + +## Fix + +Replace `m_collisionDisabledObjects` with a pointer hash set. + +```cpp +// btSoftBody.h — change: +// btAlignedObjectArray m_collisionDisabledObjects; +// to: +#include "LinearMath/btHashMap.h" +btHashMap m_collisionDisabledSet; + +// btSoftBody.cpp — appendAnchor: +if (disableCollisionBetweenLinkedBodies) +{ + btHashPtr key(body); + if (!m_collisionDisabledSet.find(key)) + m_collisionDisabledSet.insert(key, true); +} + +// btSoftRigidCollisionAlgorithm.cpp: +btHashPtr key(rigidCollisionObjectWrap->getCollisionObject()); +if (!softBody->m_collisionDisabledSet.find(key)) +{ + softBody->getSoftBodySolver()->processCollision(softBody, rigidCollisionObjectWrap); +} +``` + +## Speedup + +| D (disabled objects) | C (overlapping rigids) | Before ops/step | After ops/step | Speedup | +|----------------------|------------------------|-----------------|----------------|---------| +| 10 | 20 | 200 | 20 | 10× | +| 50 | 100 | 5,000 | 100 | 50× | +| 100 | 200 | 20,000 | 200 | 100× | + +Cloth and rope simulations with many anchor points in scenes with many rigid +bodies are the primary beneficiaries. diff --git a/defects/dry/patch/CLEAN.md b/defects/dry/patch/CLEAN.md new file mode 100644 index 000000000..30ed7a51b --- /dev/null +++ b/defects/dry/patch/CLEAN.md @@ -0,0 +1,28 @@ +# dry (Urho3D fork) — CWE-407 Scan Result: CLEAN + +**Scanned:** 2026-03-29 +**Repo:** https://github.com/nikibobi/dry (Urho3D fork) + +## Findings + +All hot-path dedup structures in dry use `HashSet` or `HashMap`: + +| Hot Path | Container | O() | +|----------|-----------|-----| +| `Renderer::DrawDebugGeometry` processedGeometries/processedLights | `HashSet` / `HashSet` | O(1) | +| `AnimationController::Update` processedAnimations | `HashSet` | O(1) | +| `PhysicsWorld` currentCollisions / previousCollisions | `HashMap, ManifoldPair>` | O(1) | +| `Scene` node/component registries | `HashMap` | O(1) | + +`PODVector::Contains` in `PhysicsQueryCallback::addSingleResult` +is O(R) per contact point, but R is bounded by the number of bodies in the +query volume (typically < 10 in practice) and the method is not called +per-frame unless the game explicitly issues repeated overlap queries. + +Node tag lookup `HasTag()` uses `StringVector::Contains` (O(T)), but T (tags +per node) is typically 1-3. Scene-level `GetNodesWithTag()` is backed by +`HashMap>` — O(1) tag lookup. + +No qualifying O(N²) defects found in any hot path. + +**Verdict: CLEAN**