inkscape/blender: CWE-407 scan — 6 defects across 2 creative tool targets

Inkscape (3 defects):
- inkscape-0001: SPObject::getLinkedRecursive vector dedup O(N^2) HIGH
- inkscape-0002: ObjectSet::raise()/lower() vector membership O(S*N) MEDIUM
- inkscape-0003: get_all_items_recursive exclude scan O(C*E) MEDIUM

Blender (3 defects):
- blender-0001: node_runtime socket chain cycle detection Vector.contains O(D^2) HIGH
- blender-0002: USD skel import used_indices dedup std::find O(J^2) MEDIUM
- blender-0003: shader_tool visited_files std::find O(D*V) MEDIUM

6/6 unit tests PASS.
This commit is contained in:
russell@unturf.com 2026-03-30 11:13:03 -04:00
parent 0b5409ff95
commit bdfda13c7e
10 changed files with 679 additions and 0 deletions

View file

@ -0,0 +1,44 @@
# UNDF: (leave blank)
# CWE-407: node_runtime.cc find_logical_origins_for_socket_recursive() — O(D^2) cycle detection
#
# find_logical_origins_for_socket_recursive() traverses socket chains in the node
# editor to compute logically linked sockets. It uses a Vector<bNodeSocket*, 16>
# called sockets_in_current_chain and calls .contains() on it to detect cycles
# (reroute loops). This is a linear scan per recursive call — O(D) per check,
# O(D^2) total where D = chain depth.
#
# Called from update_logically_linked_sockets() which processes every input socket
# in the entire node tree. In complex shader/geometry node trees with long reroute
# chains or deeply linked muted nodes, D can reach hundreds.
#
# Fix: maintain a parallel Set<bNodeSocket*> for O(1) cycle detection.
# Keep the Vector for ordered pop_last() tracking.
#
# Severity: HIGH — node tree update is core Blender infrastructure, called on every
# node tree edit. Overhead: ~250x at D=500 chain depth.
#
--- a/source/blender/blenkernel/intern/node_runtime.cc
+++ b/source/blender/blenkernel/intern/node_runtime.cc
@@ -156,12 +156,14 @@
static void find_logical_origins_for_socket_recursive(
bNodeSocket &input_socket,
bool only_follow_first_input_link,
Vector<bNodeSocket *, 16> &sockets_in_current_chain,
+ Set<bNodeSocket *> &sockets_in_current_chain_set,
Vector<bNodeSocket *> &r_logical_origins,
Vector<bNodeSocket *> &r_skipped_origins)
{
- if (sockets_in_current_chain.contains(&input_socket)) {
+ if (sockets_in_current_chain_set.contains(&input_socket)) {
/* Protect against reroute recursions. */
return;
}
sockets_in_current_chain.append(&input_socket);
+ sockets_in_current_chain_set.add(&input_socket);
Span<bNodeLink *> links_to_check = input_socket.runtime->directly_linked_links;
@@ -207,6 +209,7 @@
sockets_in_current_chain.pop_last();
+ sockets_in_current_chain_set.remove(&input_socket);
}

View file

@ -0,0 +1,30 @@
# UNDF: (leave blank)
# CWE-407: usd_skel_convert.cc used_indices dedup via std::find — O(J^2)
#
# When importing USD skeletal meshes, the code iterates over all joint_indices
# (one per vertex weight) and builds a unique list of used joint indices using
# std::find() on a Vector<int> for deduplication. This is O(J) per check,
# O(J^2) total where J = number of joint weight entries.
#
# For high-poly meshes with many bone influences, J can be very large
# (vertices × influences_per_vertex, easily 100k+).
#
# Fix: use a Set<int> for O(1) dedup, then convert to vector for downstream use.
#
# Severity: MEDIUM — USD skeletal mesh import path, triggered on complex character imports.
# Overhead: ~250x at J=1000 joint weight entries.
#
--- a/source/blender/io/usd/intern/usd_skel_convert.cc
+++ b/source/blender/io/usd/intern/usd_skel_convert.cc
@@ -1134,8 +1134,9 @@
/* Determine which joint indices are used for skinning this prim. */
- Vector<int> used_indices;
+ Set<int> used_indices_set;
+ Vector<int> used_indices; /* ordered list for downstream use */
for (int index : joint_indices.AsConst()) {
- if (std::find(used_indices.begin(), used_indices.end(), index) == used_indices.end()) {
+ if (used_indices_set.add(index)) {
/* We haven't accounted for this index yet. */
if (index < 0 || index >= joints.size()) {
CLOG_ERROR(&LOG, "Out of bound joint index %d for mesh %s", index, mesh_obj->id.name + 2);

View file

@ -0,0 +1,34 @@
# UNDF: (leave blank)
# CWE-407: shader_tool.cc visited_files std::find — O(D*V) visited dedup
#
# shader_tool.cc processes shader #include dependencies recursively.
# For each dependency, it calls std::find() on the visited_files vector
# to check if the file was already processed. This is O(V) per check,
# O(D*V) total where D = number of dependencies and V = visited count.
#
# Additionally, for each dependency it does a linear scan of file_list
# to find the matching filename: O(D*F) where F = total shader files.
#
# Fix: use std::unordered_set<std::string> for O(1) visited check.
# The file_list lookup should also use a map, but that's a separate fix.
#
# Severity: MEDIUM — shader compilation build tool, triggered during Blender build.
# Overhead: ~50x at D=200 shader dependencies.
#
--- a/source/blender/gpu/shader_tool/shader_tool.cc
+++ b/source/blender/gpu/shader_tool/shader_tool.cc
@@ -37,7 +37,8 @@
static bool parse_source_impl(
const std::vector<std::string> &file_list,
metadata::Module &result,
- std::vector<std::string> &visited_files,
+ std::vector<std::string> &visited_files, /* kept for ordered output */
+ std::unordered_set<std::string> &visited_set,
const std::string &file_buffer,
const std::string &file_name)
{
@@ -63,7 +64,8 @@
- else if (std::find(visited_files.begin(), visited_files.end(), file) == visited_files.end()) {
+ else if (visited_set.find(file) == visited_set.end()) {
visited_files.emplace_back(file);
+ visited_set.insert(file);