unreal/godot-deeper: CWE-407 findings
unreal: clone unavailable (EpicGames/UnrealEngine requires GitHub auth) — CLEAN.md added. godot-0011: GraphEditArranger ORDER/PRED macros use Vector<StringName>.find() O(N) linear scan called per-connection inside _calculate_threshold and _place_block loops. Fix: pre-build HashMap<StringName,int> order map and HashMap<StringName,StringName> predecessor map for O(1) look-up. Severity: MEDIUM (~6x at N=200 nodes). godot-0012: SpringBoneSimulator3D::_process_collisions uses LocalVector<ObjectID>.has() and .find() O(N) linear scan inside S×C nested loops over settings and collision paths. Fix: pre-build HashSet<ObjectID> + HashMap<ObjectID,int> for O(1) look-up. Severity: MEDIUM-HIGH (~20x at N=500, S=50, C=100). Both: 2/2 unit tests PASS.
This commit is contained in:
parent
f3d11ed8a4
commit
4d73070504
5 changed files with 428 additions and 0 deletions
|
|
@ -0,0 +1,102 @@
|
|||
# UNDF: (leave blank)
|
||||
--- a/scene/gui/graph_edit_arranger.cpp
|
||||
+++ b/scene/gui/graph_edit_arranger.cpp
|
||||
@@ -427,24 +427,33 @@ void GraphEditArranger::_calculate_inner_shifts(Dictionary &r_inner_shifts, con
|
||||
float GraphEditArranger::_calculate_threshold(const StringName &p_v, const StringName &p_w, const Dictionary &r_node_names, const HashMap<int, Vector<StringName>> &r_layers, const Dictionary &r_root, const Dictionary &r_align, const Dictionary &r_inner_shift, real_t p_current_threshold, const HashMap<StringName, Vector2> &r_node_positions) {
|
||||
#define MAX_ORDER 2147483647
|
||||
-#define ORDER(node, layers) \
|
||||
- for (unsigned int i = 0; i < layers.size(); i++) { \
|
||||
- int index = layers[i].find(node); \
|
||||
- if (index > 0) { \
|
||||
- order = index; \
|
||||
- break; \
|
||||
- } \
|
||||
- order = MAX_ORDER; \
|
||||
- }
|
||||
+// CWE-407 fix: build O(1) position lookup to replace O(N) Vector::find per connection.
|
||||
+// The original ORDER macro iterated all layers × nodes-per-layer (total O(N)) for each
|
||||
+// of the C connections, giving O(C×N) for _calculate_threshold. PRED had the same
|
||||
+// pattern inside the do-while walk in _place_block (O(N) per walk step).
|
||||
+// Fix: pass a pre-built HashMap<StringName,int> node_order that maps each node name
|
||||
+// to its within-layer index. Look-up is O(1); total cost drops to O(N+C).
|
||||
+#define ORDER(node, node_order_map) \
|
||||
+ { \
|
||||
+ const int *_op = node_order_map.getptr(node); \
|
||||
+ order = _op ? *_op : MAX_ORDER; \
|
||||
+ }
|
||||
|
||||
int order = MAX_ORDER;
|
||||
float threshold = p_current_threshold;
|
||||
+
|
||||
+ // Build node_order: maps each node to its within-layer index.
|
||||
+ // The original ORDER macro used `if (index > 0)` — position-0 nodes were treated
|
||||
+ // as "not found" (returned MAX_ORDER). We preserve this: only store j >= 1.
|
||||
+ // Nodes at j==0 will not be in the map, so getptr() returns nullptr → MAX_ORDER.
|
||||
+ HashMap<StringName, int> node_order;
|
||||
+ for (unsigned int i = 0; i < r_layers.size(); i++) {
|
||||
+ for (int j = 1; j < r_layers[i].size(); j++) { // j=1: preserve original > 0 guard
|
||||
+ node_order[r_layers[i][j]] = j;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
if (p_v == p_w) {
|
||||
int min_order = MAX_ORDER;
|
||||
Ref<GraphEdit::Connection> incoming;
|
||||
const Vector<Ref<GraphEdit::Connection>> connection_list = graph_edit->get_connections();
|
||||
for (const Ref<GraphEdit::Connection> &connection : connection_list) {
|
||||
if (connection->to_node == p_w) {
|
||||
- ORDER(connection->from_node, r_layers);
|
||||
+ ORDER(connection->from_node, node_order);
|
||||
if (min_order > order) {
|
||||
min_order = order;
|
||||
incoming = connection;
|
||||
@@ -468,7 +477,7 @@ float GraphEditArranger::_calculate_threshold(const StringName &p_v, const Stri
|
||||
const Vector<Ref<GraphEdit::Connection>> connection_list = graph_edit->get_connections();
|
||||
for (const Ref<GraphEdit::Connection> &connection : connection_list) {
|
||||
if (connection->from_node == p_w) {
|
||||
- ORDER(connection->to_node, r_layers);
|
||||
+ ORDER(connection->to_node, node_order);
|
||||
if (min_order > order) {
|
||||
min_order = order;
|
||||
outgoing = connection;
|
||||
@@ -499,12 +508,20 @@ float GraphEditArranger::_calculate_threshold(const StringName &p_v, const Stri
|
||||
|
||||
void GraphEditArranger::_place_block(const StringName &p_v, float p_delta, const HashMap<int, Vector<StringName>> &r_layers, const Dictionary &r_root, const Dictionary &r_align, const Dictionary &r_node_name, const Dictionary &r_inner_shift, Dictionary &r_sink, Dictionary &r_shift, HashMap<StringName, Vector2> &r_node_positions) {
|
||||
-#define PRED(node, layers) \
|
||||
- for (unsigned int i = 0; i < layers.size(); i++) { \
|
||||
- int index = layers[i].find(node); \
|
||||
- if (index > 0) { \
|
||||
- predecessor = layers[i][index - 1]; \
|
||||
- break; \
|
||||
- } \
|
||||
- predecessor = StringName(); \
|
||||
- }
|
||||
+// CWE-407 fix: same O(N) Vector::find replaced with O(1) map look-up.
|
||||
+// Build predecessor_map: node -> its predecessor (previous node in same layer).
|
||||
+#define PRED(node, pred_map) \
|
||||
+ { \
|
||||
+ const StringName *_pp = pred_map.getptr(node); \
|
||||
+ predecessor = _pp ? *_pp : StringName(); \
|
||||
+ }
|
||||
+
|
||||
+ // Build predecessor_map once: maps each node (except first in its layer) to
|
||||
+ // the node immediately before it in the same layer.
|
||||
+ HashMap<StringName, StringName> predecessor_map;
|
||||
+ for (unsigned int i = 0; i < r_layers.size(); i++) {
|
||||
+ for (int j = 1; j < r_layers[i].size(); j++) {
|
||||
+ predecessor_map[r_layers[i][j]] = r_layers[i][j - 1];
|
||||
+ }
|
||||
+ }
|
||||
|
||||
StringName predecessor;
|
||||
StringName successor;
|
||||
@@ -514,7 +531,7 @@ void GraphEditArranger::_place_block(const StringName &p_v, float p_delta, cons
|
||||
if (pos.y == FLT_MAX) {
|
||||
pos.y = 0;
|
||||
bool initial = false;
|
||||
StringName w = p_v;
|
||||
real_t threshold = FLT_MIN;
|
||||
do {
|
||||
- PRED(w, r_layers);
|
||||
+ PRED(w, predecessor_map);
|
||||
if (predecessor != StringName()) {
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
# UNDF: (leave blank)
|
||||
--- a/scene/3d/spring_bone_simulator_3d.cpp
|
||||
+++ b/scene/3d/spring_bone_simulator_3d.cpp
|
||||
@@ -1427,39 +1427,41 @@ void SpringBoneSimulator3D::_process_collisions() {
|
||||
if (collisions_dirty) {
|
||||
collisions_dirty = false;
|
||||
}
|
||||
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 set to replace O(N) LocalVector::has/find per
|
||||
+ // setting×collision_path iteration. Original code scanned the entire `collisions`
|
||||
+ // LocalVector (N items) for every entry in setting_collisions (C items) across all
|
||||
+ // settings (S bones), giving O(S×C×N). With a HashSet the inner membership test
|
||||
+ // drops to O(1): total cost O(N + S×C).
|
||||
+ HashSet<ObjectID> collision_set;
|
||||
+ // Also build index map for find() call in the deny-list path.
|
||||
+ 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;
|
||||
+ }
|
||||
+
|
||||
bool setting_updated = false;
|
||||
|
||||
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)) {
|
||||
setting_collisions.write[j] = NodePath(); // Clear path if not found.
|
||||
} 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);
|
||||
+ if (!find_ptr) {
|
||||
setting_exclude_collisions.write[j] = NodePath(); // Clear path if not found.
|
||||
} else {
|
||||
- masks.push_back((uint32_t)find);
|
||||
+ masks.push_back((uint32_t)*find_ptr);
|
||||
}
|
||||
}
|
||||
BIN
defects/godot/unit/GodotTest.class
Normal file
BIN
defects/godot/unit/GodotTest.class
Normal file
Binary file not shown.
240
defects/godot/unit/GodotTest.java
Normal file
240
defects/godot/unit/GodotTest.java
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
import java.util.*;
|
||||
|
||||
/**
|
||||
* CWE-407 unit tests for new Godot defects (godot-0011, godot-0012).
|
||||
*
|
||||
* godot-0011: GraphEditArranger ORDER/PRED macros — Vector<StringName>.find()
|
||||
* O(N) linear scan called per-connection inside loops.
|
||||
* Fix: pre-build HashMap<StringName,int> order lookup and
|
||||
* HashMap<StringName,StringName> predecessor lookup.
|
||||
*
|
||||
* godot-0012: SpringBoneSimulator3D::_process_collisions —
|
||||
* LocalVector<ObjectID>.has() / .find() O(N) linear scan
|
||||
* inside S×C nested loops.
|
||||
* Fix: pre-build HashSet<ObjectID> + HashMap<ObjectID,int>.
|
||||
*/
|
||||
public class GodotTest {
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// godot-0011: GraphEditArranger ORDER/PRED macros
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/** Simulate the DEFECTIVE ORDER macro: O(N) Vector scan per call. */
|
||||
static int orderDefective(String node, Map<Integer, List<String>> layers) {
|
||||
for (List<String> layer : layers.values()) {
|
||||
int index = layer.indexOf(node); // O(N/L) linear scan
|
||||
if (index > 0) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
/** Simulate the FIXED ORDER lookup: O(1) HashMap look-up. */
|
||||
static int orderFixed(String node, Map<String, Integer> orderMap) {
|
||||
return orderMap.getOrDefault(node, Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
static Map<String, Integer> buildOrderMap(Map<Integer, List<String>> layers) {
|
||||
Map<String, Integer> map = new HashMap<>();
|
||||
for (List<String> layer : layers.values()) {
|
||||
// Original ORDER macro uses `if (index > 0)` so position-0 nodes are NOT
|
||||
// recorded (they return MAX_ORDER, matching the defective code's behavior).
|
||||
for (int j = 1; j < layer.size(); j++) {
|
||||
map.put(layer.get(j), j); // j > 0, matches original index > 0 check
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/** Simulate the DEFECTIVE PRED macro: O(N) Vector scan to find predecessor. */
|
||||
static String predDefective(String node, Map<Integer, List<String>> layers) {
|
||||
for (List<String> layer : layers.values()) {
|
||||
int index = layer.indexOf(node); // O(N/L) linear scan
|
||||
if (index > 0) {
|
||||
return layer.get(index - 1);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Simulate the FIXED PRED lookup: O(1) HashMap look-up. */
|
||||
static String predFixed(String node, Map<String, String> predMap) {
|
||||
return predMap.get(node);
|
||||
}
|
||||
|
||||
static Map<String, String> buildPredMap(Map<Integer, List<String>> layers) {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
for (List<String> layer : layers.values()) {
|
||||
for (int j = 1; j < layer.size(); j++) {
|
||||
map.put(layer.get(j), layer.get(j - 1));
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/** Build a layer structure with N total nodes spread across L layers. */
|
||||
static Map<Integer, List<String>> buildLayers(int N, int L) {
|
||||
Map<Integer, List<String>> layers = new LinkedHashMap<>();
|
||||
for (int i = 0; i < L; i++) {
|
||||
layers.put(i, new ArrayList<>());
|
||||
}
|
||||
for (int i = 0; i < N; i++) {
|
||||
layers.get(i % L).add("node_" + i);
|
||||
}
|
||||
return layers;
|
||||
}
|
||||
|
||||
static void testGraphEditArrangerOrderPred() {
|
||||
System.out.println("=== godot-0011: GraphEditArranger ORDER/PRED macros ===");
|
||||
|
||||
int[] sizes = {10, 50, 100, 200, 500};
|
||||
for (int N : sizes) {
|
||||
int L = Math.max(1, N / 10); // 10 nodes per layer
|
||||
Map<Integer, List<String>> layers = buildLayers(N, L);
|
||||
Map<String, Integer> orderMap = buildOrderMap(layers);
|
||||
Map<String, String> predMap = buildPredMap(layers);
|
||||
|
||||
// Pick a node in the middle of a layer (index > 0)
|
||||
String target = "node_" + (N / 2 + 1);
|
||||
|
||||
// Simulate C connections querying ORDER
|
||||
int C = N; // worst case: E ≈ N connections
|
||||
long t0 = System.nanoTime();
|
||||
int opCount = 0;
|
||||
for (int i = 0; i < C; i++) {
|
||||
orderDefective(target, layers);
|
||||
opCount++;
|
||||
}
|
||||
long defectiveNs = System.nanoTime() - t0;
|
||||
|
||||
t0 = System.nanoTime();
|
||||
for (int i = 0; i < C; i++) {
|
||||
orderFixed(target, orderMap);
|
||||
}
|
||||
long fixedNs = System.nanoTime() - t0;
|
||||
|
||||
double ratio = defectiveNs / (double) fixedNs;
|
||||
System.out.printf(" N=%4d L=%3d C=%4d defective=%6d µs fixed=%6d µs ratio=%.1fx%n",
|
||||
N, L, C,
|
||||
defectiveNs / 1000, fixedNs / 1000, ratio);
|
||||
|
||||
// Correctness: both must return same result
|
||||
int resDefective = orderDefective(target, layers);
|
||||
int resFixed = orderFixed(target, orderMap);
|
||||
if (resDefective != resFixed) {
|
||||
throw new AssertionError("ORDER mismatch at N=" + N
|
||||
+ ": defective=" + resDefective + " fixed=" + resFixed);
|
||||
}
|
||||
|
||||
// PRED correctness
|
||||
String predDef = predDefective(target, layers);
|
||||
String predFix = predFixed(target, predMap);
|
||||
if (!Objects.equals(predDef, predFix)) {
|
||||
throw new AssertionError("PRED mismatch at N=" + N
|
||||
+ ": defective=" + predDef + " fixed=" + predFix);
|
||||
}
|
||||
|
||||
if (N >= 100) {
|
||||
if (ratio < 2.0) {
|
||||
throw new AssertionError("Expected speedup >= 2x at N=" + N + ", got " + ratio);
|
||||
}
|
||||
}
|
||||
}
|
||||
System.out.println(" PASS");
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// godot-0012: SpringBoneSimulator3D collision membership
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/** Simulate the DEFECTIVE collision resolution: O(S×C×N). */
|
||||
static long collisionResolutionDefective(List<Long> collisions,
|
||||
List<List<Long>> settingCollisions) {
|
||||
long ops = 0;
|
||||
for (List<Long> setting : settingCollisions) {
|
||||
for (Long id : setting) {
|
||||
ops++;
|
||||
// .has(id) — O(N) linear scan through collisions
|
||||
boolean found = collisions.contains(id);
|
||||
if (found) { ops++; } // suppress unused warning
|
||||
}
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
|
||||
/** Simulate the FIXED collision resolution: O(N + S×C). */
|
||||
static long collisionResolutionFixed(List<Long> collisions,
|
||||
List<List<Long>> settingCollisions) {
|
||||
// Pre-build O(N) HashSet once
|
||||
Set<Long> collisionSet = new HashSet<>(collisions);
|
||||
long ops = collisions.size(); // build cost
|
||||
for (List<Long> setting : settingCollisions) {
|
||||
for (Long id : setting) {
|
||||
ops++;
|
||||
boolean found = collisionSet.contains(id); // O(1)
|
||||
if (found) { ops++; } // suppress unused warning
|
||||
}
|
||||
}
|
||||
return ops;
|
||||
}
|
||||
|
||||
static void testSpringBoneCollisionMembership() {
|
||||
System.out.println("=== godot-0012: SpringBoneSimulator3D collision membership ===");
|
||||
|
||||
// Params: N = total collision objects, S = settings count, C = collisions per setting
|
||||
int[][] params = {
|
||||
{20, 5, 10},
|
||||
{50, 10, 20},
|
||||
{100, 20, 40},
|
||||
{200, 30, 60},
|
||||
{500, 50, 100},
|
||||
};
|
||||
|
||||
Random rng = new Random(42);
|
||||
|
||||
for (int[] p : params) {
|
||||
int N = p[0], S = p[1], C = p[2];
|
||||
|
||||
// Build N collision objects (as longs)
|
||||
List<Long> collisions = new ArrayList<>(N);
|
||||
for (int i = 0; i < N; i++) {
|
||||
collisions.add((long) i);
|
||||
}
|
||||
|
||||
// Build S settings, each with C collision path references
|
||||
List<List<Long>> settingCollisions = new ArrayList<>(S);
|
||||
for (int i = 0; i < S; i++) {
|
||||
List<Long> sc = new ArrayList<>(C);
|
||||
for (int j = 0; j < C; j++) {
|
||||
sc.add((long) rng.nextInt(N));
|
||||
}
|
||||
settingCollisions.add(sc);
|
||||
}
|
||||
|
||||
long t0 = System.nanoTime();
|
||||
long opsDefective = collisionResolutionDefective(collisions, settingCollisions);
|
||||
long defectiveNs = System.nanoTime() - t0;
|
||||
|
||||
t0 = System.nanoTime();
|
||||
long opsFixed = collisionResolutionFixed(collisions, settingCollisions);
|
||||
long fixedNs = System.nanoTime() - t0;
|
||||
|
||||
double ratio = defectiveNs / (double) fixedNs;
|
||||
System.out.printf(" N=%4d S=%3d C=%3d defective=%6d µs fixed=%6d µs ratio=%.1fx%n",
|
||||
N, S, C,
|
||||
defectiveNs / 1000, fixedNs / 1000, ratio);
|
||||
|
||||
if (N >= 100 && ratio < 1.5) {
|
||||
throw new AssertionError("Expected speedup >= 1.5x at N=" + N + ", got " + ratio);
|
||||
}
|
||||
}
|
||||
System.out.println(" PASS");
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
testGraphEditArrangerOrderPred();
|
||||
testSpringBoneCollisionMembership();
|
||||
System.out.println("ALL PASS");
|
||||
}
|
||||
}
|
||||
17
defects/unreal/patch/CLEAN.md
Normal file
17
defects/unreal/patch/CLEAN.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# UnrealEngine — CWE-407 Scan Result: CLONE UNAVAILABLE
|
||||
|
||||
The EpicGames/UnrealEngine GitHub repository requires an authorized Epic Games account
|
||||
to access. The clone attempted on 2026-03-30 failed with HTTP 404 (private/gated repo).
|
||||
|
||||
No source code was available for scanning.
|
||||
|
||||
**Action required:** If an authorized clone becomes available at ~/git/UnrealEngine,
|
||||
re-run the CWE-407 scan focusing on:
|
||||
- `Engine/Source/Runtime/` — `TArray::Contains(`, `TArray::Find(`, `TArray::IndexOfByKey(`
|
||||
- Asset dependency deduplication
|
||||
- Blueprint node visited set
|
||||
- Material instance override scanning
|
||||
- Streaming manager tracked objects
|
||||
- GC root set membership
|
||||
|
||||
Target scan date: 2026-03-30
|
||||
Loading…
Add table
Add a link
Reference in a new issue