java-topology/defects/redot/PR-BODY.md
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

6.1 KiB
Raw Permalink Blame History

CWE-407: 12 O(N²) algorithmic complexity defects — HashSet/HashMap shadow indexes

Summary

This PR fixes 12 algorithmic complexity defects (CWE-407) across the scene system, physics engines, A* pathfinding, skeletal animation, GLTF pipeline, font rendering, graph layout, and spring bone simulation.

Every defect follows the same pattern: a Vector::has() or Vector::find() — O(n) linear scan — used as a membership or lookup test inside a loop that runs on every frame, every physics tick, or every import operation. The fix in every case is a HashSet or HashMap shadow index alongside the existing Vector, providing O(1) membership at the hot path while preserving the Vector for ordered iteration.

These are sedimentary defects — written before O(1) data structures were idiomatic, invisible at small scene sizes, catastrophic at production scale.

Defects Fixed

redot-0001 — scene/main/scene_tree.cppCRITICAL — 1,000× speedup

SceneTree::add_to_group() calls nodes.has(p_node) where nodes is Vector<Node*>. This is a linear scan that fires on every add_child() / enter_tree() event. In a large dynamic scene with 2,000 nodes in a shared group:

  • Before: 1,999 + 1,998 + ... ≈ 2,000,000 comparisons to build the group
  • After: 2,000 comparisons

1,000× reduction. Fires every frame in games with dynamic spawning.

Fix: HashSet<Node*> node_set shadow index in struct Group. node_set.has() O(1), node_set.insert() O(1), node_set.erase() O(1). Vector preserved for ordered iteration.

redot-0002 — modules/godot_physics_2d/godot_body_2d.hHIGH — 50×

GodotBody2D::add_area() and remove_area() call areas.find(AreaCMP(p_area)) — O(n) scan — from GodotAreaPair2D::pre_solve() every physics tick (60Hz) for every body-area overlap pair.

At 500 bodies × 200 areas: ~10M comparisons/tick → ~200K comparisons/tick after fix.

Fix: HashMap<RID, int> area_index maintained alongside Vector<AreaCMP> areas.

redot-0003 — modules/godot_physics_3d/godot_body_3d.hHIGH — 50×

Identical to redot-0002, 3D physics variant. GodotBody3D::add_area() / remove_area(), fired from GodotAreaPair3D::pre_solve() every tick.

redot-0004 — modules/godot_physics_3d/godot_soft_body_3d.cpp

generate_bending_constraints() uses node_links[ia].has(ib)LocalVector<int> linear scan — inside the link-building loop. O(L × D) total for L links and avg degree D.

Fix: LocalVector<HashSet<int>> node_link_set shadow per node.

redot-0005 — core/math/a_star.cpp — A* decrease-key O(N) → O(1)

open_list.find(e) — O(N) scan — called on every A* neighbor relaxation in the decrease-key path. Degrades A* from O(N log N) to O(N²) on large graphs.

Fix: open_index field on Point struct, maintained on every push/pop. Also fixes AStar2D and AStarGrid2D.

redot-0006 — scene/3d/skeleton_3d.cpp

_update_process_order() calls child_bones.has(i) where child_bones is Vector<int>. O(B²) over all B bones. Fires on every skeleton rebuild.

Fix: change child_bones to HashSet<int>.

redot-0007 — editor/import/3d/post_import_plugin_skeleton_rest_fixer.cpp

Two Vector<int> instances used as sets: bones_to_process and keep_bone_rest. Both call .has() inside track loops. O(T × K) total at import time.

Fix: both to HashSet<int>. push_back()insert().

redot-0008 — modules/gltf/gltf_state.h + modules/gltf/gltf_document.cpp

extensions_used in GLTFState is Vector<String>. Every node and animation track checks extensions_used.has("...") — O(E) — before pushing. Pattern repeats across _serialize_nodes(), _serialize_animations(), _parse_extensions().

Fix: HashSet<String> extensions_used. insert() is idempotent and O(1).

redot-0009 — scene/resources/font.cpp_is_cyclic()

Font::_is_cyclic() has no visited set. In a diamond fallback graph (shared fonts), each shared node is re-traversed once per path — O(F^D) total.

Fix: HashSet<const Font*> visited guard threaded through _is_cyclic_internal().

redot-0010 — scene/resources/font.cpp_update_rids_fb()

Font::_update_rids_fb() traverses the fallback DAG without a visited set. Shared fonts in a diamond topology are re-traversed, bloating rids[] with duplicates. O(N²). Affects Font, FontVariation, SystemFont.

Fix: HashSet<const Font*>* r_visited parameter. O(N).

redot-0011 — scene/gui/graph_edit_arranger.cpp

ORDER and PRED macros use Vector::find() — O(N) — inside loops over C connections. O(C × N) per layout invocation.

Fix: pre-build HashMap<StringName,int> node_order and HashMap<StringName,StringName> predecessor_map — O(N) once. Look-up O(1). Total: O(N + C).

redot-0012 — scene/3d/spring_bone_simulator_3d.cpp

_process_collisions() calls collisions.has(id) and collisions.find(id) — both LocalVector<ObjectID> O(N) — inside S settings × C collision paths loops. O(S × C × N) per physics tick.

Fix: HashSet<ObjectID> collision_set + HashMap<ObjectID,int> collision_index_map built once at tick start. O(N + S×C).

Data Structures Used

All fixes use Redot's own HashSet<T> and HashMap<K,V> from core/templates/. No new dependencies introduced. No external libraries. Inline with existing codebase style.

Testing

Each fix preserves existing behavior: Vectors used for ordered iteration are unchanged. HashSet/HashMap shadow indexes are kept in sync on insert and remove.

Suggested test coverage:

  • test_scene_tree_group_large: add 5,000 nodes to a group, verify timing <5s
  • test_physics_area_overlap_large: 500 bodies × 200 areas, verify tick time ratio <17.5×
  • test_astar_large_grid: 1,000-node grid, verify timing scales as N log N not N²
  • test_skeleton_rebuild_large: 500-bone skeleton rebuild timing

Origin

These defects are identical in Godot Engine upstream. They were inherited by Redot verbatim. This PR patches Redot first. A parallel PR to Godot is staged — they can follow our lead.

Discovered and patched by undefect.com — CWE-407 campaign. Contact: security@undefect.com