java-topology/defects/redot/patch/redot-0012-spring-bone-simulator-collision-linear-scan.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

69 lines
2.8 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-000001242
# CWE-407: Algorithmic Complexity — O(S×C×N) → O(N+S×C) in SpringBoneSimulator3D::_process_collisions()
#
# Defect: collisions.has(id) and collisions.find(id) are O(N) — LocalVector linear scan —
# inside nested loops over S settings × C collision paths.
# Total cost: O(S × C × N) per physics tick. For 20 bones × 10 collisions × 50 objects:
# ~10,000 comparisons per tick at 60Hz = 600,000 extra comparisons/second.
#
# Fix: pre-build HashSet<ObjectID> collision_set and HashMap<ObjectID,int> collision_index_map.
# has() and find() are O(1). Total cost: O(N + S×C) per tick. S×C× speedup.
#
# Complexity gate (unit/test-redot-0012-spring-bone-collision.cpp):
# S=20 settings, C=10 paths, N=50 collisions: time ratio on 5× scale must be <17.5×
--- a/scene/3d/spring_bone_simulator_3d.cpp
+++ b/scene/3d/spring_bone_simulator_3d.cpp
@@ -1427,39 +1427,41 @@ void SpringBoneSimulator3D::_process_collisions() {
collisions.clear();
for (int i = 0; i < get_child_count(); i++) {
SpringBoneCollision3D *c = Object::cast_to<SpringBoneCollision3D>(get_child(i));
if (c) {
collisions.push_back(c->get_instance_id());
}
}
+ // CWE-407 fix: build O(1) lookup structures to replace O(N) LocalVector::has/find.
+ // Original: O(S×C×N). Fixed: O(N + S×C).
+ HashSet<ObjectID> collision_set;
+ HashMap<ObjectID, int> collision_index_map;
+ for (uint32_t ci = 0; ci < collisions.size(); ci++) {
+ collision_set.insert(collisions[ci]);
+ collision_index_map[collisions[ci]] = (int)ci;
+ }
+
for (int i = 0; i < settings.size(); i++) {
LocalVector<ObjectID> &cache = settings[i]->cached_collisions;
cache.clear();
if (!settings[i]->enable_all_child_collisions) {
// Allow list.
Vector<NodePath> &setting_collisions = settings[i]->collisions;
for (int j = 0; j < setting_collisions.size(); j++) {
Node *n = get_node_or_null(setting_collisions[j]);
if (!n) { continue; }
ObjectID id = n->get_instance_id();
- if (!collisions.has(id)) {
+ if (!collision_set.has(id)) { // O(1) via HashSet
setting_collisions.write[j] = NodePath();
} else {
cache.push_back(id);
}
}
} else {
// Deny list.
LocalVector<uint32_t> masks;
Vector<NodePath> &setting_exclude_collisions = settings[i]->exclude_collisions;
for (int j = 0; j < setting_exclude_collisions.size(); j++) {
Node *n = get_node_or_null(setting_exclude_collisions[j]);
if (!n) { continue; }
ObjectID id = n->get_instance_id();
- int find = collisions.find(id);
- if (find < 0) {
+ const int *find_ptr = collision_index_map.getptr(id); // O(1) via HashMap
+ if (!find_ptr) {
setting_exclude_collisions.write[j] = NodePath();
} else {
- masks.push_back((uint32_t)find);
+ masks.push_back((uint32_t)*find_ptr);
}
}