diff --git a/defects/godot/patch/godot-0011-graph-edit-arranger-order-pred-linear-scan.patch b/defects/godot/patch/godot-0011-graph-edit-arranger-order-pred-linear-scan.patch new file mode 100644 index 000000000..7f9ed7c87 --- /dev/null +++ b/defects/godot/patch/godot-0011-graph-edit-arranger-order-pred-linear-scan.patch @@ -0,0 +1,102 @@ +# UNDF: (leave blank) +--- a/scene/gui/graph_edit_arranger.cpp ++++ b/scene/gui/graph_edit_arranger.cpp +@@ -427,24 +427,33 @@ void GraphEditArranger::_calculate_inner_shifts(Dictionary &r_inner_shifts, con + float GraphEditArranger::_calculate_threshold(const StringName &p_v, const StringName &p_w, const Dictionary &r_node_names, const HashMap> &r_layers, const Dictionary &r_root, const Dictionary &r_align, const Dictionary &r_inner_shift, real_t p_current_threshold, const HashMap &r_node_positions) { + #define MAX_ORDER 2147483647 +-#define ORDER(node, layers) \ +- for (unsigned int i = 0; i < layers.size(); i++) { \ +- int index = layers[i].find(node); \ +- if (index > 0) { \ +- order = index; \ +- break; \ +- } \ +- order = MAX_ORDER; \ +- } ++// CWE-407 fix: build O(1) position lookup to replace O(N) Vector::find per connection. ++// The original ORDER macro iterated all layers × nodes-per-layer (total O(N)) for each ++// of the C connections, giving O(C×N) for _calculate_threshold. PRED had the same ++// pattern inside the do-while walk in _place_block (O(N) per walk step). ++// Fix: pass a pre-built HashMap node_order that maps each node name ++// to its within-layer index. Look-up is O(1); total cost drops to O(N+C). ++#define ORDER(node, node_order_map) \ ++ { \ ++ const int *_op = node_order_map.getptr(node); \ ++ order = _op ? *_op : MAX_ORDER; \ ++ } + + int order = MAX_ORDER; + float threshold = p_current_threshold; ++ ++ // Build node_order: maps each node to its within-layer index. ++ // The original ORDER macro used `if (index > 0)` — position-0 nodes were treated ++ // as "not found" (returned MAX_ORDER). We preserve this: only store j >= 1. ++ // Nodes at j==0 will not be in the map, so getptr() returns nullptr → MAX_ORDER. ++ HashMap node_order; ++ for (unsigned int i = 0; i < r_layers.size(); i++) { ++ for (int j = 1; j < r_layers[i].size(); j++) { // j=1: preserve original > 0 guard ++ node_order[r_layers[i][j]] = j; ++ } ++ } ++ + if (p_v == p_w) { + int min_order = MAX_ORDER; + Ref incoming; + const Vector> connection_list = graph_edit->get_connections(); + for (const Ref &connection : connection_list) { + if (connection->to_node == p_w) { +- ORDER(connection->from_node, r_layers); ++ ORDER(connection->from_node, node_order); + if (min_order > order) { + min_order = order; + incoming = connection; +@@ -468,7 +477,7 @@ float GraphEditArranger::_calculate_threshold(const StringName &p_v, const Stri + const Vector> connection_list = graph_edit->get_connections(); + for (const Ref &connection : connection_list) { + if (connection->from_node == p_w) { +- ORDER(connection->to_node, r_layers); ++ ORDER(connection->to_node, node_order); + if (min_order > order) { + min_order = order; + outgoing = connection; +@@ -499,12 +508,20 @@ float GraphEditArranger::_calculate_threshold(const StringName &p_v, const Stri + + void GraphEditArranger::_place_block(const StringName &p_v, float p_delta, const HashMap> &r_layers, const Dictionary &r_root, const Dictionary &r_align, const Dictionary &r_node_name, const Dictionary &r_inner_shift, Dictionary &r_sink, Dictionary &r_shift, HashMap &r_node_positions) { +-#define PRED(node, layers) \ +- for (unsigned int i = 0; i < layers.size(); i++) { \ +- int index = layers[i].find(node); \ +- if (index > 0) { \ +- predecessor = layers[i][index - 1]; \ +- break; \ +- } \ +- predecessor = StringName(); \ +- } ++// CWE-407 fix: same O(N) Vector::find replaced with O(1) map look-up. ++// Build predecessor_map: node -> its predecessor (previous node in same layer). ++#define PRED(node, pred_map) \ ++ { \ ++ const StringName *_pp = pred_map.getptr(node); \ ++ predecessor = _pp ? *_pp : StringName(); \ ++ } ++ ++ // Build predecessor_map once: maps each node (except first in its layer) to ++ // the node immediately before it in the same layer. ++ HashMap predecessor_map; ++ for (unsigned int i = 0; i < r_layers.size(); i++) { ++ for (int j = 1; j < r_layers[i].size(); j++) { ++ predecessor_map[r_layers[i][j]] = r_layers[i][j - 1]; ++ } ++ } + + StringName predecessor; + StringName successor; +@@ -514,7 +531,7 @@ void GraphEditArranger::_place_block(const StringName &p_v, float p_delta, cons + if (pos.y == FLT_MAX) { + pos.y = 0; + bool initial = false; + StringName w = p_v; + real_t threshold = FLT_MIN; + do { +- PRED(w, r_layers); ++ PRED(w, predecessor_map); + if (predecessor != StringName()) { diff --git a/defects/godot/patch/godot-0012-spring-bone-simulator-collision-linear-scan.patch b/defects/godot/patch/godot-0012-spring-bone-simulator-collision-linear-scan.patch new file mode 100644 index 000000000..752c43775 --- /dev/null +++ b/defects/godot/patch/godot-0012-spring-bone-simulator-collision-linear-scan.patch @@ -0,0 +1,69 @@ +# UNDF: (leave blank) +--- a/scene/3d/spring_bone_simulator_3d.cpp ++++ b/scene/3d/spring_bone_simulator_3d.cpp +@@ -1427,39 +1427,41 @@ void SpringBoneSimulator3D::_process_collisions() { + if (collisions_dirty) { + collisions_dirty = false; + } + collisions.clear(); + for (int i = 0; i < get_child_count(); i++) { + SpringBoneCollision3D *c = Object::cast_to(get_child(i)); + if (c) { + collisions.push_back(c->get_instance_id()); + } + } + ++ // CWE-407 fix: build O(1) lookup set to replace O(N) LocalVector::has/find per ++ // setting×collision_path iteration. Original code scanned the entire `collisions` ++ // LocalVector (N items) for every entry in setting_collisions (C items) across all ++ // settings (S bones), giving O(S×C×N). With a HashSet the inner membership test ++ // drops to O(1): total cost O(N + S×C). ++ HashSet collision_set; ++ // Also build index map for find() call in the deny-list path. ++ HashMap collision_index_map; ++ for (uint32_t ci = 0; ci < collisions.size(); ci++) { ++ collision_set.insert(collisions[ci]); ++ collision_index_map[collisions[ci]] = (int)ci; ++ } ++ + bool setting_updated = false; + + for (int i = 0; i < settings.size(); i++) { + LocalVector &cache = settings[i]->cached_collisions; + cache.clear(); + if (!settings[i]->enable_all_child_collisions) { + // Allow list. + Vector &setting_collisions = settings[i]->collisions; + for (int j = 0; j < setting_collisions.size(); j++) { + Node *n = get_node_or_null(setting_collisions[j]); + if (!n) { + continue; + } + ObjectID id = n->get_instance_id(); +- if (!collisions.has(id)) { ++ if (!collision_set.has(id)) { + setting_collisions.write[j] = NodePath(); // Clear path if not found. + } else { + cache.push_back(id); + } + } + } else { + // Deny list. + LocalVector masks; + Vector &setting_exclude_collisions = settings[i]->exclude_collisions; + for (int j = 0; j < setting_exclude_collisions.size(); j++) { + Node *n = get_node_or_null(setting_exclude_collisions[j]); + if (!n) { + continue; + } + ObjectID id = n->get_instance_id(); +- int find = collisions.find(id); +- if (find < 0) { ++ const int *find_ptr = collision_index_map.getptr(id); ++ if (!find_ptr) { + setting_exclude_collisions.write[j] = NodePath(); // Clear path if not found. + } else { +- masks.push_back((uint32_t)find); ++ masks.push_back((uint32_t)*find_ptr); + } + } diff --git a/defects/godot/unit/GodotTest.class b/defects/godot/unit/GodotTest.class new file mode 100644 index 000000000..5e802b561 Binary files /dev/null and b/defects/godot/unit/GodotTest.class differ diff --git a/defects/godot/unit/GodotTest.java b/defects/godot/unit/GodotTest.java new file mode 100644 index 000000000..57ebf91c0 --- /dev/null +++ b/defects/godot/unit/GodotTest.java @@ -0,0 +1,240 @@ +import java.util.*; + +/** + * CWE-407 unit tests for new Godot defects (godot-0011, godot-0012). + * + * godot-0011: GraphEditArranger ORDER/PRED macros — Vector.find() + * O(N) linear scan called per-connection inside loops. + * Fix: pre-build HashMap order lookup and + * HashMap predecessor lookup. + * + * godot-0012: SpringBoneSimulator3D::_process_collisions — + * LocalVector.has() / .find() O(N) linear scan + * inside S×C nested loops. + * Fix: pre-build HashSet + HashMap. + */ +public class GodotTest { + + // ----------------------------------------------------------------------- + // godot-0011: GraphEditArranger ORDER/PRED macros + // ----------------------------------------------------------------------- + + /** Simulate the DEFECTIVE ORDER macro: O(N) Vector scan per call. */ + static int orderDefective(String node, Map> layers) { + for (List layer : layers.values()) { + int index = layer.indexOf(node); // O(N/L) linear scan + if (index > 0) { + return index; + } + } + return Integer.MAX_VALUE; + } + + /** Simulate the FIXED ORDER lookup: O(1) HashMap look-up. */ + static int orderFixed(String node, Map orderMap) { + return orderMap.getOrDefault(node, Integer.MAX_VALUE); + } + + static Map buildOrderMap(Map> layers) { + Map map = new HashMap<>(); + for (List layer : layers.values()) { + // Original ORDER macro uses `if (index > 0)` so position-0 nodes are NOT + // recorded (they return MAX_ORDER, matching the defective code's behavior). + for (int j = 1; j < layer.size(); j++) { + map.put(layer.get(j), j); // j > 0, matches original index > 0 check + } + } + return map; + } + + /** Simulate the DEFECTIVE PRED macro: O(N) Vector scan to find predecessor. */ + static String predDefective(String node, Map> layers) { + for (List layer : layers.values()) { + int index = layer.indexOf(node); // O(N/L) linear scan + if (index > 0) { + return layer.get(index - 1); + } + } + return null; + } + + /** Simulate the FIXED PRED lookup: O(1) HashMap look-up. */ + static String predFixed(String node, Map predMap) { + return predMap.get(node); + } + + static Map buildPredMap(Map> layers) { + Map map = new HashMap<>(); + for (List layer : layers.values()) { + for (int j = 1; j < layer.size(); j++) { + map.put(layer.get(j), layer.get(j - 1)); + } + } + return map; + } + + /** Build a layer structure with N total nodes spread across L layers. */ + static Map> buildLayers(int N, int L) { + Map> layers = new LinkedHashMap<>(); + for (int i = 0; i < L; i++) { + layers.put(i, new ArrayList<>()); + } + for (int i = 0; i < N; i++) { + layers.get(i % L).add("node_" + i); + } + return layers; + } + + static void testGraphEditArrangerOrderPred() { + System.out.println("=== godot-0011: GraphEditArranger ORDER/PRED macros ==="); + + int[] sizes = {10, 50, 100, 200, 500}; + for (int N : sizes) { + int L = Math.max(1, N / 10); // 10 nodes per layer + Map> layers = buildLayers(N, L); + Map orderMap = buildOrderMap(layers); + Map predMap = buildPredMap(layers); + + // Pick a node in the middle of a layer (index > 0) + String target = "node_" + (N / 2 + 1); + + // Simulate C connections querying ORDER + int C = N; // worst case: E ≈ N connections + long t0 = System.nanoTime(); + int opCount = 0; + for (int i = 0; i < C; i++) { + orderDefective(target, layers); + opCount++; + } + long defectiveNs = System.nanoTime() - t0; + + t0 = System.nanoTime(); + for (int i = 0; i < C; i++) { + orderFixed(target, orderMap); + } + long fixedNs = System.nanoTime() - t0; + + double ratio = defectiveNs / (double) fixedNs; + System.out.printf(" N=%4d L=%3d C=%4d defective=%6d µs fixed=%6d µs ratio=%.1fx%n", + N, L, C, + defectiveNs / 1000, fixedNs / 1000, ratio); + + // Correctness: both must return same result + int resDefective = orderDefective(target, layers); + int resFixed = orderFixed(target, orderMap); + if (resDefective != resFixed) { + throw new AssertionError("ORDER mismatch at N=" + N + + ": defective=" + resDefective + " fixed=" + resFixed); + } + + // PRED correctness + String predDef = predDefective(target, layers); + String predFix = predFixed(target, predMap); + if (!Objects.equals(predDef, predFix)) { + throw new AssertionError("PRED mismatch at N=" + N + + ": defective=" + predDef + " fixed=" + predFix); + } + + if (N >= 100) { + if (ratio < 2.0) { + throw new AssertionError("Expected speedup >= 2x at N=" + N + ", got " + ratio); + } + } + } + System.out.println(" PASS"); + } + + // ----------------------------------------------------------------------- + // godot-0012: SpringBoneSimulator3D collision membership + // ----------------------------------------------------------------------- + + /** Simulate the DEFECTIVE collision resolution: O(S×C×N). */ + static long collisionResolutionDefective(List collisions, + List> settingCollisions) { + long ops = 0; + for (List setting : settingCollisions) { + for (Long id : setting) { + ops++; + // .has(id) — O(N) linear scan through collisions + boolean found = collisions.contains(id); + if (found) { ops++; } // suppress unused warning + } + } + return ops; + } + + /** Simulate the FIXED collision resolution: O(N + S×C). */ + static long collisionResolutionFixed(List collisions, + List> settingCollisions) { + // Pre-build O(N) HashSet once + Set collisionSet = new HashSet<>(collisions); + long ops = collisions.size(); // build cost + for (List setting : settingCollisions) { + for (Long id : setting) { + ops++; + boolean found = collisionSet.contains(id); // O(1) + if (found) { ops++; } // suppress unused warning + } + } + return ops; + } + + static void testSpringBoneCollisionMembership() { + System.out.println("=== godot-0012: SpringBoneSimulator3D collision membership ==="); + + // Params: N = total collision objects, S = settings count, C = collisions per setting + int[][] params = { + {20, 5, 10}, + {50, 10, 20}, + {100, 20, 40}, + {200, 30, 60}, + {500, 50, 100}, + }; + + Random rng = new Random(42); + + for (int[] p : params) { + int N = p[0], S = p[1], C = p[2]; + + // Build N collision objects (as longs) + List collisions = new ArrayList<>(N); + for (int i = 0; i < N; i++) { + collisions.add((long) i); + } + + // Build S settings, each with C collision path references + List> settingCollisions = new ArrayList<>(S); + for (int i = 0; i < S; i++) { + List sc = new ArrayList<>(C); + for (int j = 0; j < C; j++) { + sc.add((long) rng.nextInt(N)); + } + settingCollisions.add(sc); + } + + long t0 = System.nanoTime(); + long opsDefective = collisionResolutionDefective(collisions, settingCollisions); + long defectiveNs = System.nanoTime() - t0; + + t0 = System.nanoTime(); + long opsFixed = collisionResolutionFixed(collisions, settingCollisions); + long fixedNs = System.nanoTime() - t0; + + double ratio = defectiveNs / (double) fixedNs; + System.out.printf(" N=%4d S=%3d C=%3d defective=%6d µs fixed=%6d µs ratio=%.1fx%n", + N, S, C, + defectiveNs / 1000, fixedNs / 1000, ratio); + + if (N >= 100 && ratio < 1.5) { + throw new AssertionError("Expected speedup >= 1.5x at N=" + N + ", got " + ratio); + } + } + System.out.println(" PASS"); + } + + public static void main(String[] args) { + testGraphEditArrangerOrderPred(); + testSpringBoneCollisionMembership(); + System.out.println("ALL PASS"); + } +} diff --git a/defects/unreal/patch/CLEAN.md b/defects/unreal/patch/CLEAN.md new file mode 100644 index 000000000..8c2622438 --- /dev/null +++ b/defects/unreal/patch/CLEAN.md @@ -0,0 +1,17 @@ +# UnrealEngine — CWE-407 Scan Result: CLONE UNAVAILABLE + +The EpicGames/UnrealEngine GitHub repository requires an authorized Epic Games account +to access. The clone attempted on 2026-03-30 failed with HTTP 404 (private/gated repo). + +No source code was available for scanning. + +**Action required:** If an authorized clone becomes available at ~/git/UnrealEngine, +re-run the CWE-407 scan focusing on: +- `Engine/Source/Runtime/` — `TArray::Contains(`, `TArray::Find(`, `TArray::IndexOfByKey(` +- Asset dependency deduplication +- Blueprint node visited set +- Material instance override scanning +- Streaming manager tracked objects +- GC root set membership + +Target scan date: 2026-03-30