python-ruby wave: cpython/mastodon defects + 8 CLEAN markers; count 668→671

cpython-0001: unittest.mock.reset_mock visited=[] O(N²) diamond traversal
mastodon-0001: OStatus Creation processed_account_ids Array#include? O(N²)
mastodon-0002: ActivityPub process_hashtag status.tags.include? O(N²) AR queries
CLEAN: dask, bottle, pyramid, alembic, dgl, synapse, dendrite, zulip
This commit is contained in:
russell@unturf.com 2026-03-29 19:50:42 -04:00
parent 0ca0eed9f9
commit e074f18ac7
5 changed files with 309 additions and 0 deletions

View file

@ -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**

View file

@ -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**

View file

@ -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 | 3742, 4954, 7580, 9095 |
| 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<btCollisionObject*>` (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<btCollisionObject*> m_overlappingObjects;
btHashMap<btHashPtr, int> 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<btHashPtr, bool> 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.

View file

@ -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<const btCollisionObject*>` (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<const class btCollisionObject*> m_collisionDisabledObjects;
// to:
#include "LinearMath/btHashMap.h"
btHashMap<btHashPtr, bool> 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.

View file

@ -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<T>` or `HashMap<K,V>`:
| Hot Path | Container | O() |
|----------|-----------|-----|
| `Renderer::DrawDebugGeometry` processedGeometries/processedLights | `HashSet<Drawable*>` / `HashSet<Light*>` | O(1) |
| `AnimationController::Update` processedAnimations | `HashSet<StringHash>` | O(1) |
| `PhysicsWorld` currentCollisions / previousCollisions | `HashMap<Pair<...>, ManifoldPair>` | O(1) |
| `Scene` node/component registries | `HashMap<unsigned, Node*>` | O(1) |
`PODVector<RigidBody*>::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<StringHash, PODVector<Node*>>` — O(1) tag lookup.
No qualifying O(N²) defects found in any hot path.
**Verdict: CLEAN**