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.
73 lines
2.9 KiB
Diff
73 lines
2.9 KiB
Diff
# UNDF: UNDF-2026-000001239
|
||
# CWE-407: Algorithmic Complexity — O(F^D) → O(F) in Font::_is_cyclic()
|
||
#
|
||
# Defect: _is_cyclic() recurses through all fallback fonts with no visited set.
|
||
# In a diamond fallback topology (shared fonts), each shared node is re-traversed
|
||
# once per path that reaches it. Total cost: O(F^D) where F = fallback count,
|
||
# D = max depth. Fires on every Font::set_fallbacks() call.
|
||
#
|
||
# Fix: add HashSet<const Font*> visited guard passed through recursion.
|
||
# Already-visited fonts are skipped. Total cost: O(F). F^D → F speedup.
|
||
#
|
||
# Complexity gate (unit/test-redot-0009-font-cyclic.cpp):
|
||
# F=20 fallbacks, depth=3 (diamond): time ratio on 5× scale must be <17.5×
|
||
|
||
--- a/scene/resources/font.h
|
||
+++ b/scene/resources/font.h
|
||
@@ -31,6 +31,7 @@
|
||
#include "core/io/resource.h"
|
||
#include "core/templates/hash_map.h"
|
||
+#include "core/templates/hash_set.h"
|
||
#include "core/templates/list.h"
|
||
#include "core/variant/typed_array.h"
|
||
#include "servers/text_server.h"
|
||
|
||
@@ -97,8 +97,9 @@ class Font : public Resource {
|
||
static void _bind_methods();
|
||
|
||
virtual void _update_rids_fb(const Font *p_f, int p_depth) const;
|
||
virtual void _update_rids() const;
|
||
virtual void reset_state() override;
|
||
|
||
- virtual bool _is_cyclic(const Ref<Font> &p_f, int p_depth) const;
|
||
- virtual bool _is_base_cyclic(const Ref<Font> &p_f, int p_depth) const;
|
||
+ // FIX redot-0009: add visited HashSet to avoid O(F^D) re-traversal
|
||
+ virtual bool _is_cyclic(const Ref<Font> &p_f, int p_depth) const;
|
||
+ bool _is_cyclic_internal(const Ref<Font> &p_f, int p_depth, HashSet<const Font *> &r_visited) const;
|
||
+ virtual bool _is_base_cyclic(const Ref<Font> &p_f, int p_depth) const;
|
||
virtual void _invalidate_rids();
|
||
|
||
--- a/scene/resources/font.cpp
|
||
+++ b/scene/resources/font.cpp
|
||
@@ -134,16 +134,28 @@ void Font::_invalidate_rids() {
|
||
|
||
bool Font::_is_cyclic(const Ref<Font> &p_f, int p_depth) const {
|
||
- ERR_FAIL_COND_V(p_depth > MAX_FALLBACK_DEPTH, true);
|
||
- if (p_f.is_null()) { return false; }
|
||
- if (p_f == this) { return true; }
|
||
- for (int i = 0; i < p_f->fallbacks.size(); i++) {
|
||
- const Ref<Font> &f = p_f->fallbacks[i];
|
||
- if (_is_cyclic(f, p_depth + 1)) { return true; }
|
||
- }
|
||
- return false;
|
||
+ // FIX redot-0009: was O(F^D) — shared fonts in diamond fallback graph re-traversed
|
||
+ // exponentially; now O(F) with visited set.
|
||
+ HashSet<const Font *> visited;
|
||
+ return _is_cyclic_internal(p_f, p_depth, visited);
|
||
+}
|
||
+
|
||
+bool Font::_is_cyclic_internal(const Ref<Font> &p_f, int p_depth, HashSet<const Font *> &r_visited) const {
|
||
+ ERR_FAIL_COND_V(p_depth > MAX_FALLBACK_DEPTH, true);
|
||
+ if (p_f.is_null()) { return false; }
|
||
+ if (p_f == this) { return true; }
|
||
+ const Font *raw = p_f.ptr();
|
||
+ if (r_visited.has(raw)) {
|
||
+ return false; // already proven non-cyclic from this node
|
||
+ }
|
||
+ r_visited.insert(raw);
|
||
+ for (int i = 0; i < p_f->fallbacks.size(); i++) {
|
||
+ const Ref<Font> &f = p_f->fallbacks[i];
|
||
+ if (_is_cyclic_internal(f, p_depth + 1, r_visited)) { return true; }
|
||
+ }
|
||
+ return false;
|
||
}
|