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
154
defects/three.js/unit/ThreeJsTest.java
Normal file
154
defects/three.js/unit/ThreeJsTest.java
Normal 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");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue