# UNDF: UNDF-2026-000000468 # threejs-0007: Node.traverse() — diamond recursion O(2^D) ## Classification | Field | Value | |-------------|-------| | CWE | CWE-407 Inefficient Algorithmic Complexity | | Severity | MEDIUM | | Component | `src/nodes/core/Node.js:351` | | Function | `Node.traverse()` | | Hot path | Called during shader material compilation — `ContextNode.getFlowContextData()`, `TSLCore.defined()`, `UniformNode.uniform()`, `RangeNode` setup | | Status | PATCHED (unit test PASS) | ## Defect `Node.traverse()` recursively visits child nodes of a shader node graph with **no visited set**. TSL (Three Shader Language) node graphs are DAGs, not trees — the same node instance can be referenced by multiple parents. On a **diamond DAG** (`A → {B, C}`, `B → D`, `C → D`), calling `A.traverse(cb)` invokes `D.traverse(cb)` **twice** — once via B and once via C. At depth D, the bottom node receives `2^D` callback invocations. ```javascript // src/nodes/core/Node.js:351 traverse( callback ) { callback( this ); for ( const childNode of this.getChildren() ) { childNode.traverse( callback ); // NO visited set — exponential on diamonds } } ``` ### Why diamond DAGs occur TSL explicitly encourages node reuse. The same uniform/texture node can appear as an input to multiple operators: ```javascript const sharedUniform = uniform( vec3( 1, 0, 0 ) ); // sharedUniform is now a child of BOTH mul and add nodes: const result = add( mul( sharedUniform, factor ), div( sharedUniform, 2.0 ) ); // result.traverse(cb) → sharedUniform visited TWICE ``` Any `context()` node wrapping a complex TSL expression tree can produce a deep diamond DAG during `getFlowContextData()`. ### Call sites | Site | When triggered | |------|---------------| | `ContextNode.getFlowContextData()` | Each shader build (per material compilation) | | `TSLCore.defined(value)` | TSL condition evaluation during material setup | | `UniformNode.uniform(value)` | Uniform creation with node value | | `RangeNode.setup()` | Range node setup during build | ### Complexity | Topology | Visits to bottom node | |----------|-----------------------| | Chain of length N | 1 | | Diamond depth 1 | 2 | | Diamond depth 3 | 8 | | Diamond depth 5 | 32 | | Diamond depth 10 | 1 024 | | Diamond depth D | 2^D | ## Benchmark (Java simulation) ``` D=1 diamond: defective=5 fixed=4 ratio=1.3x D=5 diamond: defective=125 fixed=16 ratio=7.8x D=10 diamond: defective=4093 fixed=31 ratio=132x TSL shared-uniform diamond: defective: 7 visits (sharedUniform visited twice) fixed: 6 visits (each node once) ``` Unit test: 10/10 PASS (`defects/threejs/unit/unit/ThreeJSNodeTraverseTest.java`) ## Fix Pass an optional `visited` Set through the recursion. Default to a fresh Set when called at the root (no API breakage for existing callers). ```javascript // FIXED traverse( callback, visited = new Set() ) { if ( visited.has( this ) ) return; // skip already-visited nodes visited.add( this ); callback( this ); for ( const childNode of this.getChildren() ) { childNode.traverse( callback, visited ); } } ``` All four call sites (`ContextNode`, `TSLCore`, `UniformNode`, `RangeNode`) call `traverse(callback)` with no second argument — they automatically get a new `visited` Set per traversal root, requiring zero changes at call sites.