java-topology/defects/redot/patch/redot-0001-scene-tree-group-hashset.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

58 lines
2.2 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-000001231
# CWE-407: Algorithmic Complexity — O(N²) → O(N) in SceneTree::add_to_group()
#
# Defect: E->value.nodes.has(p_node) is O(n) — Vector linear scan —
# inside add_to_group(), which fires on every add_child() / enter_tree() event.
# In large dynamic scenes with thousands of nodes in shared groups ("enemies",
# "pickable", "save_data"), this fires every frame.
# Total cost: O(N²) to add N nodes to a group. At n=2,000: ~2,000,000 comparisons.
#
# Fix: add HashSet<Node*> node_set shadow index to struct Group.
# has()/insert()/erase() all O(1). Vector preserved for ordered iteration.
# Total cost after: O(N). At n=2,000: ~2,000 comparisons.
#
# Complexity gate (unit/test-redot-0001-scene-tree-group.cpp):
# k-scaling 5×: time ratio must be <17.5× (O(k) ≈5×, not O(k²) ≈25×)
# Scale n=2,000 nodes: must complete in <2s
# Speedup at n=2,000: 1,000×
--- a/scene/main/scene_tree.h
+++ b/scene/main/scene_tree.h
@@ -123,6 +123,7 @@ class SceneTree : public MainLoop {
struct Group {
Vector<Node *> nodes;
+ HashSet<Node *> node_set; // O(1) membership — shadow index for Vector
bool changed = false;
};
--- a/scene/main/scene_tree.cpp
+++ b/scene/main/scene_tree.cpp
@@ -172,14 +172,16 @@ SceneTree::Group *SceneTree::add_to_group(const StringName &p_group, Node *p_node) {
if (!E) {
E = group_map.insert(p_group, Group());
}
- ERR_FAIL_COND_V_MSG(E->value.nodes.has(p_node), &E->value, "Already in group: " + p_group + ".");
+ // FIX redot-0001: was nodes.has(p_node) — O(n) linear scan, CWE-407
+ // nodes.has() uses Vector linear scan: O(n) per add_to_group call.
+ // node_set provides O(1) lookup. Vector preserved for ordered iteration.
+ ERR_FAIL_COND_V_MSG(E->value.node_set.has(p_node), &E->value, "Already in group: " + p_group + ".");
E->value.nodes.push_back(p_node);
+ E->value.node_set.insert(p_node);
E->value.changed = true;
return &E->value;
}
void SceneTree::remove_from_group(const StringName &p_group, Node *p_node) {
_THREAD_SAFE_METHOD_
HashMap<StringName, Group>::Iterator E = group_map.find(p_group);
ERR_FAIL_COND(!E);
E->value.nodes.erase(p_node);
+ E->value.node_set.erase(p_node);
if (E->value.nodes.is_empty()) {
group_map.remove(E);
}
}