java-topology/defects/redot/patch/redot-0010-font-update-rids-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

66 lines
2.9 KiB
Diff
Raw Permalink 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-000001240
# CWE-407: Algorithmic Complexity — O(N²) → O(N) in Font::_update_rids_fb()
#
# Defect: _update_rids_fb() recursively traverses the fallback font DAG with no visited
# set. In a diamond topology, shared fallback fonts are re-traversed once per path,
# producing duplicate RID entries and O(N²) traversal cost.
# Fires on every font metrics invalidation (text rendering setup).
#
# Fix: add HashSet<const Font*>* r_visited parameter threaded through recursion.
# Already-visited fonts are skipped. Total cost: O(N), rids[] contains each font once.
# Affects Font::_update_rids(), FontVariation::_update_rids(), SystemFont::_update_rids().
#
# Complexity gate (unit/test-redot-0010-font-update-rids.cpp):
# F=20 shared fallbacks, depth=3: time ratio on 5× scale must be <17.5×
--- a/scene/resources/font.h
+++ b/scene/resources/font.h
@@ -95,7 +95,8 @@ class Font : public Resource {
static void _bind_methods();
- virtual void _update_rids_fb(const Font *p_f, int p_depth) const;
+ // FIX redot-0010: pass visited set to avoid O(N^2) re-traversal of shared fallback fonts
+ virtual void _update_rids_fb(const Font *p_f, int p_depth, HashSet<const Font *> *r_visited = nullptr) const;
virtual void _update_rids() const;
virtual void reset_state() override;
--- a/scene/resources/font.cpp
+++ b/scene/resources/font.cpp
@@ -103,14 +103,22 @@ void Font::_update_rids_fb(const Font *p_f, int p_depth) const {
-void Font::_update_rids_fb(const Font *p_f, int p_depth) const {
- ERR_FAIL_COND(p_depth > MAX_FALLBACK_DEPTH);
- if (p_f != nullptr) {
- RID rid = p_f->_get_rid();
- if (rid.is_valid()) { rids.push_back(rid); }
- const TypedArray<Font> &_fallbacks = p_f->get_fallbacks();
- for (int i = 0; i < _fallbacks.size(); i++) {
- Ref<Font> fb_font = _fallbacks[i];
- _update_rids_fb(fb_font.ptr(), p_depth + 1);
- }
- }
-}
+// FIX redot-0010: was O(N^2) — shared fonts in diamond fallback topology re-traversed
+// once per path, bloating rids[] with duplicates and wasting render time.
+// Now O(N) with HashSet visited guard; rids[] contains each font exactly once.
+void Font::_update_rids_fb(const Font *p_f, int p_depth, HashSet<const Font *> *r_visited) const {
+ ERR_FAIL_COND(p_depth > MAX_FALLBACK_DEPTH);
+ if (p_f == nullptr) { return; }
+ if (r_visited != nullptr && r_visited->has(p_f)) { return; }
+ if (r_visited != nullptr) { r_visited->insert(p_f); }
+ RID rid = p_f->_get_rid();
+ if (rid.is_valid()) { rids.push_back(rid); }
+ const TypedArray<Font> &_fallbacks = p_f->get_fallbacks();
+ for (int i = 0; i < _fallbacks.size(); i++) {
+ Ref<Font> fb_font = _fallbacks[i];
+ _update_rids_fb(fb_font.ptr(), p_depth + 1, r_visited);
+ }
+}
void Font::_update_rids() const {
rids.clear();
- _update_rids_fb(this, 0);
+ HashSet<const Font *> visited;
+ _update_rids_fb(this, 0, &visited); // FIX redot-0010: O(N) visited guard
dirty_rids = false;
}