java-topology/defects/bullet3/unit/Bullet3Test.java
russell@unturf.com db92c9428a bullet3/three.js: CWE-407 findings
bullet3-0001: btGhostObject::addOverlappingObjectInternal uses
findLinearSearch (O(N) pointer scan) on every broadphase pair-update
callback. With N objects overlapping a ghost, each tick is O(N²). Fix:
btHashMap<btHashPtr,int> shadow index → O(1) add/remove. 250x at N=500.

three-0001: NodeBuilder.addNode/addSequentialNode use Array.includes
(O(N)) per node during shader build traversal → O(N²). StackNode.generate
uses nodes.indexOf inside filter → O(N²). Fix: shadow with Set → O(1). 250x
at N=500.
2026-03-30 09:25:29 -04:00

138 lines
5 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import java.util.*;
/**
* CWE-407 unit test for Bullet Physics btGhostObject defect.
*
* bullet3-0001: src/BulletCollision/CollisionDispatch/btGhostObject.cpp
* btGhostObject::addOverlappingObjectInternal() and removeOverlappingObjectInternal()
* use btAlignedObjectArray::findLinearSearch (O(N) pointer scan) on every broadphase
* pair-update callback. With N objects overlapping a single ghost, each physics tick
* calls addOverlappingObjectInternal N times → O(N²) total per tick.
* The source comment even acknowledges the problem:
* "if this linearSearch becomes too slow (too many overlapping objects)
* we should add a more appropriate data structure"
* Fix: maintain a btHashMap<btHashPtr,int> shadow index → O(1) add/remove.
*
* Severity: HIGH — called every broadphase tick for every ghost-object pair.
* At N=500 overlapping objects the per-tick work grows 500×.
*/
public class Bullet3Test {
// --- defect simulation: linear array dedup ---
static boolean addOverlapping_linear(List<Long> overlapping, long ptr) {
// O(N) scan — models findLinearSearch
for (long p : overlapping) {
if (p == ptr) return false; // already present
}
overlapping.add(ptr);
return true;
}
static boolean removeOverlapping_linear(List<Long> overlapping, long ptr) {
int index = -1;
for (int i = 0; i < overlapping.size(); i++) {
if (overlapping.get(i) == ptr) { index = i; break; }
}
if (index < 0) return false;
// swap with last
overlapping.set(index, overlapping.get(overlapping.size() - 1));
overlapping.remove(overlapping.size() - 1);
return true;
}
// --- fix simulation: HashMap dedup ---
static boolean addOverlapping_hash(List<Long> overlapping, Map<Long,Integer> index, long ptr) {
if (index.containsKey(ptr)) return false; // O(1)
index.put(ptr, overlapping.size());
overlapping.add(ptr);
return true;
}
static boolean removeOverlapping_hash(List<Long> overlapping, Map<Long,Integer> index, long ptr) {
Integer idx = index.get(ptr); // O(1)
if (idx == null) return false;
int lastI = overlapping.size() - 1;
if (idx != lastI) {
long moved = overlapping.get(lastI);
overlapping.set(idx, moved);
index.put(moved, idx);
}
overlapping.remove(lastI);
index.remove(ptr);
return true;
}
// --- correctness check ---
static void assertEqualSets(List<Long> a, List<Long> b, String label) throws Exception {
Set<Long> sa = new HashSet<>(a);
Set<Long> sb = new HashSet<>(b);
if (!sa.equals(sb)) {
throw new Exception("FAIL " + label + ": sets differ. a=" + sa + " b=" + sb);
}
}
static void testBullet30001() throws Exception {
int N = 500;
// Build base state: N objects in ghost overlap list
List<Long> linearList = new ArrayList<>();
List<Long> hashList = new ArrayList<>();
Map<Long,Integer> hashIdx = new HashMap<>();
for (long i = 0; i < N; i++) {
addOverlapping_linear(linearList, i);
addOverlapping_hash(hashList, hashIdx, i);
}
assertEqualSets(linearList, hashList, "initial fill");
// Attempt to re-add all (dedup test) — N² work in linear, O(N) in hash
long linearOps = 0, hashOps = 0;
for (long i = 0; i < N; i++) {
// linear: scan all N before deciding "already there"
boolean found = false;
for (long p : linearList) { linearOps++; if (p == i) { found = true; break; } }
// hash: O(1)
hashOps++;
addOverlapping_hash(hashList, hashIdx, i);
}
assertEqualSets(linearList, hashList, "after re-add");
// Remove even-indexed objects
for (long i = 0; i < N; i += 2) {
removeOverlapping_linear(linearList, i);
removeOverlapping_hash(hashList, hashIdx, i);
}
assertEqualSets(linearList, hashList, "after remove evens");
// Verify index consistency
for (int i = 0; i < hashList.size(); i++) {
long ptr = hashList.get(i);
Integer stored = hashIdx.get(ptr);
if (stored == null || stored != i) {
throw new Exception("FAIL index consistency at i=" + i + " ptr=" + ptr + " stored=" + stored);
}
}
// Performance ratio
double ratio = (double) linearOps / hashOps;
System.out.printf("bullet3-0001 PASS N=%d linear_ops=%d hash_ops=%d ratio=%.1fx%n",
N, linearOps, hashOps, ratio);
if (ratio < 2.0) {
throw new Exception("FAIL: expected ratio >= 2x, got " + ratio);
}
}
public static void main(String[] args) throws Exception {
testBullet30001();
System.out.println("All bullet3 tests PASS");
}
}