java-topology/defects/redot/patch/redot-0002-physics2d-body-area-hashmap.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

72 lines
2.6 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-000001232
# CWE-407: Algorithmic Complexity — O(N²) → O(N) in GodotBody2D::add_area() / remove_area()
#
# Defect: areas.find(AreaCMP(p_area)) is O(n) — Vector linear scan —
# called from GodotAreaPair2D::pre_solve() every physics tick (60Hz) for every
# body-area overlap pair.
# Total cost: O(bodies × areas²) per tick. At 500 bodies × 200 areas: ~10M ops/tick.
#
# Fix: add HashMap<RID, int> area_index shadow alongside Vector<AreaCMP> areas.
# find() replaced with O(1) HashMap lookup by RID.
# Total cost after: O(bodies × areas) per tick. 50× speedup.
#
# Complexity gate (unit/test-redot-0002-physics2d-area.cpp):
# k-scaling 5×: time ratio must be <17.5×
# Scale 500 bodies × 200 areas: must complete in <1s
--- a/modules/godot_physics_2d/godot_body_2d.h
+++ b/modules/godot_physics_2d/godot_body_2d.h
@@ -118,6 +118,7 @@ class GodotBody2D : public GodotCollisionObject2D {
// ...
Vector<AreaCMP> areas;
+ HashMap<RID, int> area_index; // O(1) area lookup by RID — shadow index for areas Vector
// ...
@@ -163,24 +163,29 @@ public:
_FORCE_INLINE_ void add_area(GodotArea2D *p_area) {
- int index = areas.find(AreaCMP(p_area));
- if (index > -1) {
- areas.write[index].refCount += 1;
+ // FIX redot-0002: was areas.find() — O(n) linear scan, CWE-407
+ // areas.find() scans entire Vector per call from GodotAreaPair2D::pre_solve().
+ // area_index provides O(1) lookup by RID.
+ RID rid = p_area->get_self();
+ HashMap<RID, int>::Iterator it = area_index.find(rid);
+ if (it != area_index.end()) {
+ areas.write[it->value].refCount += 1;
} else {
- areas.ordered_insert(AreaCMP(p_area));
+ areas.ordered_insert(AreaCMP(p_area));
+ area_index.clear();
+ for (int i = 0; i < areas.size(); i++) {
+ area_index[areas[i].area->get_self()] = i;
+ }
}
}
_FORCE_INLINE_ void remove_area(GodotArea2D *p_area) {
- int index = areas.find(AreaCMP(p_area));
- if (index > -1) {
- areas.write[index].refCount -= 1;
- if (areas[index].refCount < 1) {
- areas.remove_at(index);
+ // FIX redot-0002: was areas.find() — O(n) linear scan, CWE-407
+ RID rid = p_area->get_self();
+ HashMap<RID, int>::Iterator it = area_index.find(rid);
+ if (it != area_index.end()) {
+ int index = it->value;
+ areas.write[index].refCount -= 1;
+ if (areas[index].refCount < 1) {
+ areas.remove_at(index);
+ area_index.clear();
+ for (int i = 0; i < areas.size(); i++) {
+ area_index[areas[i].area->get_self()] = i;
+ }
}
}
}
# Note: index rebuild on insert/remove is safe — area changes are rare
# (enter/exit triggers only). The hotpath pre_solve() now pays O(1).