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.
This commit is contained in:
parent
99e1a862bd
commit
db92c9428a
4 changed files with 447 additions and 0 deletions
138
defects/bullet3/unit/Bullet3Test.java
Normal file
138
defects/bullet3/unit/Bullet3Test.java
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
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");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue