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:
russell@unturf.com 2026-03-30 09:25:29 -04:00
parent 99e1a862bd
commit db92c9428a
4 changed files with 447 additions and 0 deletions

View file

@ -0,0 +1,154 @@
import java.util.*;
/**
* CWE-407 unit test for three.js NodeBuilder defect.
*
* three-0001: src/nodes/core/NodeBuilder.js addNode() / addSequentialNode()
* src/nodes/core/StackNode.js generate()
*
* NodeBuilder.addNode() guards deduplication with:
* if ( this.nodes.includes( node ) === false ) { ... }
* Array.includes() is O(N). It is called once per node during the shader
* build traversal (Node.build builder.addNode), making the full build
* O(N²) in the number of unique nodes.
*
* addSequentialNode() has the identical pattern with sequentialNodes.
*
* StackNode.generate() compounds this:
* const newNodes = this.nodes.filter( n => nodes.indexOf(n) === -1 );
* filter is O(N), indexOf is O(N) per element O(N²) to compute the
* newly-added-node delta.
*
* Fix: shadow Array with Set for O(1) has() checks.
*
* Severity: MEDIUM called during shader compilation (material first-frame
* or material.needsUpdate). Complex scenes with N=300+ nodes (particles,
* post-processing chains) stall on first render due to quadratic build cost.
*
* Speedup: O(N²) O(N); measured ~250x op-count reduction at N=500.
*/
public class ThreeJsTest {
// --- defect simulation: Array.includes dedup (O(N) per call) ---
static void addNode_array(List<Object> nodes, Object node) {
if (!nodes.contains(node)) { // O(N)
nodes.add(node);
}
}
// Build simulation: traverse N nodes, each calling addNode
static long buildShader_array(List<Object> allNodes) {
List<Object> visited = new ArrayList<>();
long ops = 0;
for (Object node : allNodes) {
// simulate nodes.includes(node): scan visited
boolean found = false;
for (Object v : visited) { ops++; if (v == node) { found = true; break; } }
if (!found) {
visited.add(node);
ops++; // the push
}
}
return ops;
}
// --- fix simulation: Set.has dedup (O(1) per call) ---
static void addNode_set(List<Object> nodes, Set<Object> nodeSet, Object node) {
if (!nodeSet.contains(node)) { // O(1)
nodes.add(node);
nodeSet.add(node);
}
}
static long buildShader_set(List<Object> allNodes) {
List<Object> visited = new ArrayList<>();
Set<Object> visitedSet = new HashSet<>();
long ops = 0;
for (Object node : allNodes) {
ops++; // O(1) set lookup
if (!visitedSet.contains(node)) {
visited.add(node);
visitedSet.add(node);
}
}
return ops;
}
// --- filter delta simulation: nodes.indexOf vs Set.has ---
static long filterDelta_indexOf(List<Object> allNodes, List<Object> snapshot) {
long ops = 0;
List<Object> newNodes = new ArrayList<>();
for (Object node : allNodes) {
// indexOf O(N) per element
boolean found = false;
for (Object s : snapshot) { ops++; if (s == node) { found = true; break; } }
if (!found) newNodes.add(node);
}
return ops;
}
static long filterDelta_set(List<Object> allNodes, List<Object> snapshot) {
long ops = 0;
Set<Object> snapshotSet = new HashSet<>(snapshot);
List<Object> newNodes = new ArrayList<>();
for (Object node : allNodes) {
ops++; // O(1) set lookup
if (!snapshotSet.contains(node)) newNodes.add(node);
}
return ops;
}
static void testThree0001() throws Exception {
int N = 500;
// Build N unique node objects
List<Object> allNodes = new ArrayList<>();
for (int i = 0; i < N; i++) allNodes.add(new Object());
// Shader build dedup
long arrayOps = buildShader_array(allNodes);
long setOps = buildShader_set(allNodes);
double buildRatio = (double) arrayOps / setOps;
System.out.printf("three-0001 addNode N=%d array_ops=%d set_ops=%d ratio=%.1fx%n",
N, arrayOps, setOps, buildRatio);
if (buildRatio < 2.0) {
throw new Exception("FAIL: addNode ratio expected >= 2x, got " + buildRatio);
}
// StackNode.generate filter-delta
// snapshot = first half of nodes, allNodes = all N
List<Object> snapshot = new ArrayList<>(allNodes.subList(0, N / 2));
long idxOps = filterDelta_indexOf(allNodes, snapshot);
long setOps2 = filterDelta_set(allNodes, snapshot);
double filterRatio = (double) idxOps / setOps2;
System.out.printf("three-0001 filter N=%d indexOf_ops=%d set_ops=%d ratio=%.1fx%n",
N, idxOps, setOps2, filterRatio);
if (filterRatio < 2.0) {
throw new Exception("FAIL: filter ratio expected >= 2x, got " + filterRatio);
}
// Correctness: final visited sets must be equal
List<Object> arrayVisited = new ArrayList<>();
List<Object> setVisited = new ArrayList<>();
Set<Object> setMirror = new HashSet<>();
for (Object node : allNodes) {
addNode_array(arrayVisited, node);
addNode_set(setVisited, setMirror, node);
}
if (!new HashSet<>(arrayVisited).equals(new HashSet<>(setVisited))) {
throw new Exception("FAIL: visited sets differ between array and set implementations");
}
System.out.println("three-0001 PASS");
}
public static void main(String[] args) throws Exception {
testThree0001();
System.out.println("All three.js tests PASS");
}
}