java-topology/defects/redot/patch/redot-0011-graph-edit-arranger-order-pred-linear-scan.patch
russell@unturf.com fdbb9a1aa9 redot: 12 CWE-407 defects, patches, outreach brief, UNDF-2026-000001231..1242
All 12 O(N²) algorithmic complexity defects confirmed in Redot Engine 26.2-alpha
(commit 360a8d3). Inherited verbatim from Godot Engine upstream. All patched.

Defects span: scene group membership, 2D/3D physics area lookup, soft body
bending constraints, A* decrease-key, skeleton child bones, GLTF extension
tracking, font cyclic check, font RID traversal, graph layout ORDER/PRED
macros, and spring bone collision dispatch.

Most severe: redot-0001 fires every frame in dynamic scenes — 1,000× speedup
at n=2,000 nodes. redot-0002/0003 fire 60Hz in physics-heavy games — 50×.

Strategy: patch Redot first, Godot follows our lead.
2026-04-03 21:00:29 -04:00

77 lines
3.3 KiB
Diff
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# UNDF: UNDF-2026-000001241
# CWE-407: Algorithmic Complexity — O(C×N) → O(N+C) in GraphEditArranger layout
#
# Defect: ORDER and PRED macros use Vector::find() — O(N) — inside loops over C
# connections. ORDER is called in _calculate_threshold() for every connection.
# PRED is called in _place_block() for every walk step.
# Total cost: O(C × N) per layout pass. For a graph with 200 nodes and 500 connections:
# ~100,000 comparisons per layout invocation.
#
# Fix: pre-build HashMap<StringName,int> node_order (position within layer) and
# HashMap<StringName,StringName> predecessor_map (previous node in layer).
# ORDER and PRED look up in O(1). Total cost: O(N + C) per layout pass.
#
# Complexity gate (unit/test-redot-0011-graph-arranger.cpp):
# N=200 nodes, C=500 connections: time ratio on 5× scale must be <17.5×
--- 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, const
float GraphEditArranger::_calculate_threshold(...) {
#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.
+#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.
+ // j=1: preserve original > 0 guard — nodes at j==0 not stored → MAX_ORDER.
+ HashMap<StringName, int> node_order;
+ for (unsigned int i = 0; i < r_layers.size(); i++) {
+ for (int j = 1; j < r_layers[i].size(); j++) {
+ node_order[r_layers[i][j]] = j;
+ }
+ }
// ... connection_list loops use ORDER(node, node_order) instead of ORDER(node, r_layers)
@@ -499,12 +508,20 @@ void GraphEditArranger::_place_block(...) {
-#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: build predecessor map O(N) once; PRED look-up O(1).
+#define PRED(node, pred_map) \
+ { \
+ const StringName *_pp = pred_map.getptr(node); \
+ predecessor = _pp ? *_pp : StringName(); \
+ }
+ // Build predecessor_map: node -> previous node in its layer (or empty if first).
+ HashMap<StringName, StringName> 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];
+ }
+ }
// ... walk uses PRED(node, predecessor_map) instead of PRED(node, r_layers)