java-topology/defects/redot/patch/redot-0009-font-is-cyclic-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

73 lines
2.9 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-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;
}