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.
This commit is contained in:
parent
06a9f31583
commit
fdbb9a1aa9
16 changed files with 1148 additions and 1 deletions
181
whitepaper/outreach/redot.md
Normal file
181
whitepaper/outreach/redot.md
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
# Redot Engine — CWE-407 Disclosure Brief
|
||||
**2026-04-04 · Patches available — PR ready for upstream review**
|
||||
|
||||
## Finding
|
||||
|
||||
Twelve O(N²) defects in Redot Engine 26.2 across the scene system, 2D physics, 3D physics,
|
||||
soft body simulation, A* pathfinding, skeletal animation, GLTF import/export, font rendering,
|
||||
graph layout, and spring bone simulation. All inherited verbatim from Godot Engine upstream.
|
||||
All patched. PRs ready for review.
|
||||
|
||||
The most severe (redot-0001) fires every frame in dynamic scenes. We patched Redot first.
|
||||
Godot can follow.
|
||||
|
||||
## The Defects
|
||||
|
||||
**redot-0001 (CRITICAL — 1,000× speedup at n=2,000):** `scene/main/scene_tree.cpp:176`
|
||||
|
||||
```cpp
|
||||
// In SceneTree::add_to_group() — fires per-frame in dynamic scenes:
|
||||
ERR_FAIL_COND_V_MSG(E->value.nodes.has(p_node), &E->value, "...");
|
||||
// nodes is Vector<Node*> — .has() is O(n) linear scan
|
||||
```
|
||||
|
||||
Every `add_child()` / `enter_tree()` event fires `add_to_group()`. In large scenes with
|
||||
thousands of nodes in common groups ("enemies", "pickable", "save_data"), this fires every
|
||||
frame. Fix: `HashSet<Node*> node_set` shadow index in `struct Group`. O(1) membership.
|
||||
|
||||
**redot-0002 (HIGH — 50× speedup):** `modules/godot_physics_2d/godot_body_2d.h:167`
|
||||
|
||||
```cpp
|
||||
// In GodotBody2D::add_area() — fires per physics tick:
|
||||
int index = areas.find(AreaCMP(p_area)); // Vector<AreaCMP>.find() — O(n)
|
||||
```
|
||||
|
||||
Fires from `GodotAreaPair2D::pre_solve()` every physics tick for every body-area overlap.
|
||||
Fix: `HashMap<RID, int> area_index` shadow index. O(1) lookup by RID.
|
||||
|
||||
**redot-0003 (HIGH — 50× speedup):** `modules/godot_physics_3d/godot_body_3d.h:161`
|
||||
|
||||
Identical to redot-0002, 3D physics variant. Fires from `GodotAreaPair3D::pre_solve()`.
|
||||
|
||||
**redot-0004 (MEDIUM — 4× speedup):** `modules/godot_physics_3d/godot_soft_body_3d.cpp:665`
|
||||
|
||||
```cpp
|
||||
// In generate_bending_constraints() — fires at soft body mesh load:
|
||||
if (!node_links[ia].has(ib)) { ... } // LocalVector<int>.has() — O(n)
|
||||
```
|
||||
|
||||
Fix: `LocalVector<HashSet<int>> node_link_set` shadow index per node.
|
||||
|
||||
**redot-0005 (HIGH — O(N²)→O(N log N)):** `core/math/a_star.cpp:382`
|
||||
|
||||
```cpp
|
||||
// In AStar3D::_solve() decrease-key path:
|
||||
sorter.push_heap(0, open_list.find(e), 0, e, open_list.ptr()); // O(N) find
|
||||
```
|
||||
|
||||
Fires for every A* neighbor relaxation. Fix: `open_index` field on Point struct. O(1).
|
||||
Also affects `AStar2D` and `AStarGrid2D`.
|
||||
|
||||
**redot-0006 (MEDIUM):** `scene/3d/skeleton_3d.cpp:237`
|
||||
|
||||
```cpp
|
||||
// In Skeleton3D::_update_process_order():
|
||||
if (!bonesptr[parent_bone_idx].child_bones.has(i)) { // Vector<int>.has() — O(B)
|
||||
bonesptr[parent_bone_idx].child_bones.push_back(i);
|
||||
```
|
||||
|
||||
Fix: change `child_bones` from `Vector<int>` to `HashSet<int>`.
|
||||
|
||||
**redot-0007 (MEDIUM):** `editor/import/3d/post_import_plugin_skeleton_rest_fixer.cpp:203`
|
||||
|
||||
```cpp
|
||||
// In PostImportPluginSkeletonRestFixer::internal_process():
|
||||
if (bones_to_process.has(bone_idx)) { // Vector<int>.has() — O(K) inside O(T) loop
|
||||
```
|
||||
|
||||
Two separate Vector-as-set instances. Fix: both to `HashSet<int>`. O(T×K) → O(T+K).
|
||||
|
||||
**redot-0008 (MEDIUM):** `modules/gltf/gltf_state.h` + `modules/gltf/gltf_document.cpp:445`
|
||||
|
||||
```cpp
|
||||
// In GLTFDocument::_serialize_nodes() — fires per node per GLTF export:
|
||||
if (!p_state->extensions_used.has("KHR_node_visibility")) { // Vector<String>.has() — O(E)
|
||||
p_state->extensions_used.push_back("KHR_node_visibility");
|
||||
```
|
||||
|
||||
Fix: `extensions_used` from `Vector<String>` to `HashSet<String>` in `GLTFState`. Idempotent
|
||||
`insert()` replaces the has()+push_back() guard pattern. O(N×E) → O(N).
|
||||
|
||||
**redot-0009 (MEDIUM):** `scene/resources/font.cpp:136`
|
||||
|
||||
```cpp
|
||||
// In Font::_is_cyclic() — no visited set, O(F^D) in diamond fallback graphs:
|
||||
if (_is_cyclic(f, p_depth + 1)) { return true; } // re-traverses shared nodes
|
||||
```
|
||||
|
||||
Fix: `HashSet<const Font*> visited` guard threaded through recursion. O(F^D) → O(F).
|
||||
|
||||
**redot-0010 (MEDIUM):** `scene/resources/font.cpp:105`
|
||||
|
||||
```cpp
|
||||
// In Font::_update_rids_fb() — no visited set, diamond re-traversal bloats rids[]:
|
||||
_update_rids_fb(fb_font.ptr(), p_depth + 1); // O(N^2), duplicates in rids[]
|
||||
```
|
||||
|
||||
Fix: `HashSet<const Font*>* r_visited` parameter. O(N²) → O(N). Also fixes
|
||||
`FontVariation::_update_rids()` and `SystemFont::_update_rids()`.
|
||||
|
||||
**redot-0011 (MEDIUM):** `scene/gui/graph_edit_arranger.cpp:433`
|
||||
|
||||
```cpp
|
||||
// ORDER macro in _calculate_threshold() — O(N) Vector::find per connection:
|
||||
#define ORDER(node, layers) for (...) { int index = layers[i].find(node); ... }
|
||||
// PRED macro in _place_block() — same pattern
|
||||
```
|
||||
|
||||
Fix: pre-build `HashMap<StringName,int> node_order` and `HashMap<StringName,StringName>
|
||||
predecessor_map`. O(C×N) → O(N+C) per layout pass.
|
||||
|
||||
**redot-0012 (MEDIUM):** `scene/3d/spring_bone_simulator_3d.cpp:1455`
|
||||
|
||||
```cpp
|
||||
// In SpringBoneSimulator3D::_process_collisions() — O(S×C×N) per tick:
|
||||
if (!collisions.has(id)) { ... } // LocalVector<ObjectID>.has() — O(N)
|
||||
int find = collisions.find(id); // O(N)
|
||||
```
|
||||
|
||||
Fix: pre-build `HashSet<ObjectID> collision_set` + `HashMap<ObjectID,int>
|
||||
collision_index_map`. O(S×C×N) → O(N + S×C) per tick.
|
||||
|
||||
## Complexity Proof — Most Severe (redot-0001)
|
||||
|
||||
At group size n=2,000:
|
||||
- Defective: 1,999 + 1,998 + ... ≈ 2,000,000 comparisons to build group
|
||||
- Fixed: 2,000 comparisons (HashSet shadow)
|
||||
- **1,000× op reduction. Fires every frame in dynamic scenes.**
|
||||
|
||||
At n=2,000 nodes, 60fps: 120 million extra comparisons/second eliminated.
|
||||
|
||||
## Impact
|
||||
|
||||
Redot Engine 26.2 is a community fork of Godot, targeting developers dissatisfied with
|
||||
Godot governance. It has inherited the complete Godot 4.x codebase including all 12 of
|
||||
these defects.
|
||||
|
||||
redot-0001 is the most severe: any game with dynamic enemy spawning, item pickup systems,
|
||||
or procedurally generated content hits this path every frame. Physics-heavy games hit
|
||||
redot-0002/0003 at 60Hz. Spring-bone characters (character rigs with cloth/hair simulation)
|
||||
hit redot-0012 every tick.
|
||||
|
||||
These defects exist in every game shipped with Redot. They are invisible until scenes grow
|
||||
large — classic sedimentary defect signature.
|
||||
|
||||
## The Fix
|
||||
|
||||
All 12 patches available:
|
||||
|
||||
| Defect | File | Fix | Speedup |
|
||||
|--------|------|-----|---------|
|
||||
| redot-0001 | `scene/main/scene_tree.cpp` | HashSet shadow in Group | 1,000× at n=2,000 |
|
||||
| redot-0002 | `modules/godot_physics_2d/godot_body_2d.h` | HashMap<RID,int> shadow | 50× at 500×200 |
|
||||
| redot-0003 | `modules/godot_physics_3d/godot_body_3d.h` | HashMap<RID,int> shadow | 50× at 500×200 |
|
||||
| redot-0004 | `modules/godot_physics_3d/godot_soft_body_3d.cpp` | HashSet per node | 4× at D=4 |
|
||||
| redot-0005 | `core/math/a_star.cpp` | open_index field | O(N²)→O(N log N) |
|
||||
| redot-0006 | `scene/3d/skeleton_3d.cpp` | HashSet child_bones | B× at dense skeletons |
|
||||
| redot-0007 | `editor/import/3d/post_import_plugin_skeleton_rest_fixer.cpp` | HashSet bones | T× at import |
|
||||
| redot-0008 | `modules/gltf/gltf_state.h` | HashSet extensions_used | E× per GLTF op |
|
||||
| redot-0009 | `scene/resources/font.cpp` | visited HashSet | F^D→F |
|
||||
| redot-0010 | `scene/resources/font.cpp` | visited HashSet | N²→N |
|
||||
| redot-0011 | `scene/gui/graph_edit_arranger.cpp` | HashMap position maps | C×N→N+C |
|
||||
| redot-0012 | `scene/3d/spring_bone_simulator_3d.cpp` | HashSet+HashMap | S×C×N→N+S×C |
|
||||
|
||||
## Note on Godot Upstream
|
||||
|
||||
These defects are identical in Godot Engine and Redot Engine — both carry the same code.
|
||||
We are disclosing to Redot first. Godot can follow our lead.
|
||||
|
||||
Our patches are ready. Fork is staged. PRs will be submitted on coordinated disclosure date.
|
||||
|
||||
Contact: security@undefect.com
|
||||
Loading…
Add table
Add a link
Reference in a new issue