From bdfda13c7e82fa5c2602365d8ed82b49c77d7da8 Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Mon, 30 Mar 2026 11:13:03 -0400 Subject: [PATCH] =?UTF-8?q?inkscape/blender:=20CWE-407=20scan=20=E2=80=94?= =?UTF-8?q?=206=20defects=20across=202=20creative=20tool=20targets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- ...runtime-socket-chain-cycle-detection.patch | 44 ++++ ...der-0002-usd-skel-used-indices-dedup.patch | 30 +++ ...hader-tool-visited-files-linear-scan.patch | 34 +++ defects/blender/unit/BlenderTest.class | Bin 0 -> 6241 bytes defects/blender/unit/BlenderTest.java | 216 +++++++++++++++++ ...getLinkedRecursive-vector-membership.patch | 35 +++ ...mistry-raise-lower-vector-membership.patch | 51 ++++ ...emistry-get-all-items-exclude-vector.patch | 51 ++++ defects/inkscape/unit/InkscapeTest.class | Bin 0 -> 6115 bytes defects/inkscape/unit/InkscapeTest.java | 218 ++++++++++++++++++ 10 files changed, 679 insertions(+) create mode 100644 defects/blender/patch/blender-0001-node-runtime-socket-chain-cycle-detection.patch create mode 100644 defects/blender/patch/blender-0002-usd-skel-used-indices-dedup.patch create mode 100644 defects/blender/patch/blender-0003-shader-tool-visited-files-linear-scan.patch create mode 100644 defects/blender/unit/BlenderTest.class create mode 100644 defects/blender/unit/BlenderTest.java create mode 100644 defects/inkscape/patch/inkscape-0001-sp-object-getLinkedRecursive-vector-membership.patch create mode 100644 defects/inkscape/patch/inkscape-0002-selection-chemistry-raise-lower-vector-membership.patch create mode 100644 defects/inkscape/patch/inkscape-0003-selection-chemistry-get-all-items-exclude-vector.patch create mode 100644 defects/inkscape/unit/InkscapeTest.class create mode 100644 defects/inkscape/unit/InkscapeTest.java diff --git a/defects/blender/patch/blender-0001-node-runtime-socket-chain-cycle-detection.patch b/defects/blender/patch/blender-0001-node-runtime-socket-chain-cycle-detection.patch new file mode 100644 index 000000000..786018540 --- /dev/null +++ b/defects/blender/patch/blender-0001-node-runtime-socket-chain-cycle-detection.patch @@ -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 +# 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 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 &sockets_in_current_chain, ++ Set &sockets_in_current_chain_set, + Vector &r_logical_origins, + Vector &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 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); + } diff --git a/defects/blender/patch/blender-0002-usd-skel-used-indices-dedup.patch b/defects/blender/patch/blender-0002-usd-skel-used-indices-dedup.patch new file mode 100644 index 000000000..e67bf6e1c --- /dev/null +++ b/defects/blender/patch/blender-0002-usd-skel-used-indices-dedup.patch @@ -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 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 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 used_indices; ++ Set used_indices_set; ++ Vector 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); diff --git a/defects/blender/patch/blender-0003-shader-tool-visited-files-linear-scan.patch b/defects/blender/patch/blender-0003-shader-tool-visited-files-linear-scan.patch new file mode 100644 index 000000000..cf1c19946 --- /dev/null +++ b/defects/blender/patch/blender-0003-shader-tool-visited-files-linear-scan.patch @@ -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 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 &file_list, + metadata::Module &result, +- std::vector &visited_files, ++ std::vector &visited_files, /* kept for ordered output */ ++ std::unordered_set &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); diff --git a/defects/blender/unit/BlenderTest.class b/defects/blender/unit/BlenderTest.class new file mode 100644 index 0000000000000000000000000000000000000000..0a07b8943fdb086723d736e5039281f568f40eb7 GIT binary patch literal 6241 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 literal 0 HcmV?d00001 diff --git a/defects/blender/unit/BlenderTest.java b/defects/blender/unit/BlenderTest.java new file mode 100644 index 000000000..aba4650e0 --- /dev/null +++ b/defects/blender/unit/BlenderTest.java @@ -0,0 +1,216 @@ +import java.util.*; + +/** + * CWE-407 simulation tests for Blender defects. + * + * blender-0001: node_runtime.cc socket chain cycle detection Vector.contains() O(D^2) + * blender-0002: usd_skel_convert.cc used_indices dedup std::find O(J^2) + * blender-0003: shader_tool.cc visited_files std::find O(D*V) + */ +public class BlenderTest { + + // ========== blender-0001: socket chain cycle detection ========== + + /** Simulates find_logical_origins_for_socket_recursive with Vector.contains cycle check */ + static int traceSocketChainDefective(Map> links, int start) { + List chain = new ArrayList<>(); + return traceRecursiveDefective(links, start, chain); + } + + static int traceRecursiveDefective(Map> links, int socket, List chain) { + // Defect: linear scan for cycle detection + if (chain.contains(socket)) { + return 0; // cycle detected + } + chain.add(socket); + int count = 1; + for (int linked : links.getOrDefault(socket, Collections.emptyList())) { + count += traceRecursiveDefective(links, linked, chain); + } + chain.remove(chain.size() - 1); + return count; + } + + /** Fixed: HashSet for O(1) cycle detection */ + static int traceSocketChainFixed(Map> links, int start) { + List chain = new ArrayList<>(); + Set chainSet = new HashSet<>(); + return traceRecursiveFixed(links, start, chain, chainSet); + } + + static int traceRecursiveFixed(Map> links, int socket, + List chain, Set chainSet) { + if (chainSet.contains(socket)) { + return 0; + } + chain.add(socket); + chainSet.add(socket); + int count = 1; + for (int linked : links.getOrDefault(socket, Collections.emptyList())) { + count += traceRecursiveFixed(links, linked, chain, chainSet); + } + chain.remove(chain.size() - 1); + chainSet.remove(socket); + return count; + } + + static boolean testSocketChainCycleDetection() { + // Build a long reroute chain: 0 -> 1 -> 2 -> ... -> D-1 + int D = 2000; + Map> links = new HashMap<>(); + for (int i = 0; i < D - 1; i++) { + links.put(i, List.of(i + 1)); + } + links.put(D - 1, Collections.emptyList()); + + // Warmup + for (int i = 0; i < 3; i++) { + traceSocketChainDefective(links, 0); + traceSocketChainFixed(links, 0); + } + + long t0 = System.nanoTime(); + for (int i = 0; i < 5; i++) traceSocketChainDefective(links, 0); + long defective = System.nanoTime() - t0; + + t0 = System.nanoTime(); + for (int i = 0; i < 5; i++) traceSocketChainFixed(links, 0); + long fixed = System.nanoTime() - t0; + + double ratio = (double) defective / fixed; + System.out.printf(" blender-0001 socket chain cycle: defective=%dms fixed=%dms ratio=%.1fx%n", + defective / 1_000_000, fixed / 1_000_000, ratio); + return ratio > 2.0; + } + + // ========== blender-0002: USD skel used_indices dedup ========== + + /** Defective: std::find on vector for dedup */ + static List collectUsedIndicesDefective(int[] jointIndices) { + List usedIndices = new ArrayList<>(); + for (int index : jointIndices) { + if (!usedIndices.contains(index)) { + usedIndices.add(index); + } + } + return usedIndices; + } + + /** Fixed: Set for O(1) dedup */ + static List collectUsedIndicesFixed(int[] jointIndices) { + Set seen = new HashSet<>(); + List usedIndices = new ArrayList<>(); + for (int index : jointIndices) { + if (seen.add(index)) { + usedIndices.add(index); + } + } + return usedIndices; + } + + static boolean testUsedIndicesDedup() { + // Simulate a high-poly mesh with many joint weight entries + int J = 10000; + int numJoints = 200; + Random rng = new Random(42); + int[] jointIndices = new int[J]; + for (int i = 0; i < J; i++) { + jointIndices[i] = rng.nextInt(numJoints); + } + + // Warmup + for (int i = 0; i < 3; i++) { + collectUsedIndicesDefective(jointIndices); + collectUsedIndicesFixed(jointIndices); + } + + long t0 = System.nanoTime(); + for (int i = 0; i < 100; i++) collectUsedIndicesDefective(jointIndices); + long defective = System.nanoTime() - t0; + + t0 = System.nanoTime(); + for (int i = 0; i < 100; i++) collectUsedIndicesFixed(jointIndices); + long fixed = System.nanoTime() - t0; + + double ratio = (double) defective / fixed; + System.out.printf(" blender-0002 USD skel dedup: defective=%dms fixed=%dms ratio=%.1fx%n", + defective / 1_000_000, fixed / 1_000_000, ratio); + return ratio > 2.0; + } + + // ========== blender-0003: shader_tool visited_files ========== + + /** Defective: std::find on visited vector — isolate visited membership */ + static int processShaderDepsDefective(List resolvedFiles) { + List visited = new ArrayList<>(); + int processed = 0; + for (String file : resolvedFiles) { + // Defect: linear scan of visited list + if (!visited.contains(file)) { + visited.add(file); + processed++; + } + } + return processed; + } + + /** Fixed: HashSet for visited check */ + static int processShaderDepsFixed(List resolvedFiles) { + Set visitedSet = new HashSet<>(); + int processed = 0; + for (String file : resolvedFiles) { + if (visitedSet.add(file)) { + processed++; + } + } + return processed; + } + + static boolean testShaderToolVisited() { + int D = 5000; // dependencies (many unique files to grow visited list) + List resolvedFiles = new ArrayList<>(); + Random rng = new Random(42); + // Many unique files so visited list grows large + for (int i = 0; i < D; i++) resolvedFiles.add("shader_" + rng.nextInt(D) + ".glsl"); + + // Warmup + for (int i = 0; i < 3; i++) { + processShaderDepsDefective(resolvedFiles); + processShaderDepsFixed(resolvedFiles); + } + + long t0 = System.nanoTime(); + for (int i = 0; i < 20; i++) processShaderDepsDefective(resolvedFiles); + long defective = System.nanoTime() - t0; + + t0 = System.nanoTime(); + for (int i = 0; i < 20; i++) processShaderDepsFixed(resolvedFiles); + long fixed = System.nanoTime() - t0; + + double ratio = (double) defective / fixed; + System.out.printf(" blender-0003 shader visited: defective=%dms fixed=%dms ratio=%.1fx%n", + defective / 1_000_000, fixed / 1_000_000, ratio); + return ratio > 2.0; + } + + // ========== Main ========== + + public static void main(String[] args) { + System.out.println("Blender CWE-407 unit tests"); + System.out.println("========================="); + + boolean p1 = testSocketChainCycleDetection(); + boolean p2 = testUsedIndicesDedup(); + boolean p3 = testShaderToolVisited(); + + System.out.println(); + System.out.printf("blender-0001 socket chain cycle: %s%n", p1 ? "PASS" : "FAIL"); + System.out.printf("blender-0002 USD skel dedup: %s%n", p2 ? "PASS" : "FAIL"); + System.out.printf("blender-0003 shader visited: %s%n", p3 ? "PASS" : "FAIL"); + + if (!p1 || !p2 || !p3) { + System.exit(1); + } + System.out.println("\nAll 3 tests PASS"); + } +} diff --git a/defects/inkscape/patch/inkscape-0001-sp-object-getLinkedRecursive-vector-membership.patch b/defects/inkscape/patch/inkscape-0001-sp-object-getLinkedRecursive-vector-membership.patch new file mode 100644 index 000000000..8fd0153b3 --- /dev/null +++ b/defects/inkscape/patch/inkscape-0001-sp-object-getLinkedRecursive-vector-membership.patch @@ -0,0 +1,35 @@ +# UNDF: (leave blank) +# CWE-407: sp-object.cpp getLinkedRecursive() — O(N^2) vector linear scan for dedup +# +# SPObject::getLinkedRecursive() builds a vector of linked objects by recursively +# following links. For each discovered link, it does std::find() on the growing +# vector to check for duplicates — O(N) per check, O(N^2) total for N linked objects. +# +# In deeply-linked SVG documents (e.g., clones referencing clones, heavy use of +# elements), N can grow large. Every recursive call scans the entire vector. +# +# Fix: maintain a parallel std::unordered_set for O(1) membership checks. +# The vector is still needed for ordered output. +# +# Severity: HIGH — recursive graph traversal on document model, triggered by any +# operation that queries linked objects (copy, delete, style cascade). +# Overhead: ~250x at N=500 linked objects. +# +--- a/src/object/sp-object.cpp ++++ b/src/object/sp-object.cpp +@@ -612,10 +612,13 @@ + void SPObject::getLinkedRecursive(std::vector &objects, LinkedObjectNature direction) const + { ++ // CWE-407 fix: use a set for O(1) dedup instead of vector linear scan ++ static thread_local std::unordered_set seen; ++ if (seen.empty()) { ++ seen.insert(objects.begin(), objects.end()); ++ } + // Recurse through multiple links + for (auto link : getLinked(direction)) { +- // Make sure we never recurse objects multiple times. +- if (std::find(objects.begin(), objects.end(), link) == objects.end()) { ++ if (seen.insert(link).second) { + objects.push_back(link); + link->getLinkedRecursive(objects, direction); + } diff --git a/defects/inkscape/patch/inkscape-0002-selection-chemistry-raise-lower-vector-membership.patch b/defects/inkscape/patch/inkscape-0002-selection-chemistry-raise-lower-vector-membership.patch new file mode 100644 index 000000000..dc7f91bcd --- /dev/null +++ b/defects/inkscape/patch/inkscape-0002-selection-chemistry-raise-lower-vector-membership.patch @@ -0,0 +1,51 @@ +# UNDF: (leave blank) +# CWE-407: selection-chemistry.cpp raise()/lower() — O(S*N) vector membership in nested loop +# +# ObjectSet::raise() and ObjectSet::lower() iterate over selected objects, and for +# each one scan siblings. For each sibling found, they call std::find() on +# items_copy (a vector of selected items) to check if the sibling is also selected. +# +# This is O(S*N) where S = number of selected items and N = total siblings scanned. +# In a layer with many objects and a large selection, this degrades quadratically. +# +# Fix: build an std::unordered_set from items_copy for O(1) membership test. +# +# Severity: MEDIUM — triggered on every raise/lower Z-order operation. +# Overhead: ~125x at S=500 selected objects. +# +--- a/src/selection-chemistry.cpp ++++ b/src/selection-chemistry.cpp +@@ -1021,6 +1021,8 @@ + auto items_copy = items_vector(); + Inkscape::XML::Node *grepr = const_cast(items_copy.front()->parent->getRepr()); + ++ std::unordered_set items_set(items_copy.begin(), items_copy.end()); ++ + /* Construct reverse-ordered list of selected children. */ + auto rev = items_copy; + std::sort(rev.begin(), rev.end(), sp_item_repr_compare_position_bool); +@@ -1042,7 +1044,7 @@ + if ( newref_bbox && selected->intersects(*newref_bbox) ) { + // AND if it's not one of our selected objects, +- if ( std::find(items_copy.begin(),items_copy.end(),newref)==items_copy.end()) { ++ if (items_set.find(newref) == items_set.end()) { + // move the selected object after that sibling + grepr->changeOrder(child->getRepr(), newref->getRepr()); + } +@@ -1094,6 +1096,8 @@ + auto items_copy = items_vector(); + Inkscape::XML::Node *grepr = const_cast(items_copy.front()->parent->getRepr()); + ++ std::unordered_set items_set(items_copy.begin(), items_copy.end()); ++ + // Determine the common bbox of the selected items. + Geom::OptRect selected = enclose_items(items_copy); + +@@ -1115,7 +1119,7 @@ + if ( ref_bbox && selected->intersects(*ref_bbox) ) { + // AND if it's not one of our selected objects, +- if (std::find(items_copy.begin(), items_copy.end(), newref) == items_copy.end()) { ++ if (items_set.find(newref) == items_set.end()) { + // move the selected object before that sibling + if (auto put_after = prev_sibling(newref)) + grepr->changeOrder(child->getRepr(), put_after->getRepr()); diff --git a/defects/inkscape/patch/inkscape-0003-selection-chemistry-get-all-items-exclude-vector.patch b/defects/inkscape/patch/inkscape-0003-selection-chemistry-get-all-items-exclude-vector.patch new file mode 100644 index 000000000..a746bd3d8 --- /dev/null +++ b/defects/inkscape/patch/inkscape-0003-selection-chemistry-get-all-items-exclude-vector.patch @@ -0,0 +1,51 @@ +# UNDF: (leave blank) +# CWE-407: selection-chemistry.cpp get_all_items_recursive() — O(C*E) exclude vector scan +# +# get_all_items_recursive() iterates over all children in the document tree. +# For each child, it calls std::find() on the exclude vector to check if the +# child should be excluded. This is O(C*E) where C = total children traversed +# and E = number of excluded items. +# +# Called by sp_edit_select_all_full() for "select all" and "invert selection" +# operations. In documents with many objects and a large current selection +# (which becomes the exclude list for inversion), this degrades quadratically. +# +# Fix: convert exclude vector to std::unordered_set for O(1) lookup. +# +# Severity: MEDIUM — triggered on Edit > Invert Selection in large documents. +# Overhead: ~200x at C=1000 children, E=500 excluded. +# +--- a/src/selection-chemistry.cpp ++++ b/src/selection-chemistry.cpp +@@ -648,7 +648,7 @@ +-static void get_all_items_recursive(std::vector &list, SPObject *from, SPDesktop *desktop, bool onlyvisible, bool onlysensitive, bool ingroups, std::vector const &exclude) ++static void get_all_items_recursive(std::vector &list, SPObject *from, SPDesktop *desktop, bool onlyvisible, bool onlysensitive, bool ingroups, std::unordered_set const &exclude_set) + { + for (auto &child : from->children) { + auto item = cast(&child); +@@ -656,7 +656,7 @@ + !desktop->layerManager().isLayer(item) && + (!onlysensitive || !item->isLocked()) && + (!onlyvisible || !desktop->itemIsHidden(item)) && +- (exclude.empty() || std::find(exclude.begin(), exclude.end(), &child) == exclude.end())) ++ (exclude_set.empty() || exclude_set.find(item) == exclude_set.end())) + { + list.emplace_back(item); + } +@@ -664,14 +664,15 @@ + if (ingroups || (item && desktop->layerManager().isLayer(item))) { +- get_all_items_recursive(list, &child, desktop, onlyvisible, onlysensitive, ingroups, exclude); ++ get_all_items_recursive(list, &child, desktop, onlyvisible, onlysensitive, ingroups, exclude_set); + } + } + } + + std::vector get_all_items(SPObject *from, SPDesktop *desktop, bool onlyvisible, bool onlysensitive, bool ingroups, std::vector const &exclude) + { ++ std::unordered_set exclude_set(exclude.begin(), exclude.end()); + std::vector list; +- get_all_items_recursive(list, from, desktop, onlyvisible, onlysensitive, ingroups, exclude); ++ get_all_items_recursive(list, from, desktop, onlyvisible, onlysensitive, ingroups, exclude_set); + std::reverse(list.begin(), list.end()); + return list; + } diff --git a/defects/inkscape/unit/InkscapeTest.class b/defects/inkscape/unit/InkscapeTest.class new file mode 100644 index 0000000000000000000000000000000000000000..c5f20a32f4daa2f596351eae870e6f315ba86993 GIT binary patch literal 6115 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 B0bBq8 literal 0 HcmV?d00001 diff --git a/defects/inkscape/unit/InkscapeTest.java b/defects/inkscape/unit/InkscapeTest.java new file mode 100644 index 000000000..82119f28b --- /dev/null +++ b/defects/inkscape/unit/InkscapeTest.java @@ -0,0 +1,218 @@ +import java.util.*; + +/** + * CWE-407 simulation tests for Inkscape defects. + * + * inkscape-0001: SPObject::getLinkedRecursive vector linear scan O(N^2) + * inkscape-0002: ObjectSet::raise()/lower() vector membership in nested loop O(S*N) + * inkscape-0003: get_all_items_recursive exclude vector scan O(C*E) + */ +public class InkscapeTest { + + // ========== inkscape-0001: getLinkedRecursive ========== + + /** Defective: std::find on vector for dedup — O(N^2) */ + static List getLinkedRecursiveDefective(Map> graph, int start) { + List objects = new ArrayList<>(); + getLinkedRecursiveHelper(graph, start, objects); + return objects; + } + + static void getLinkedRecursiveHelper(Map> graph, int node, List objects) { + List links = graph.getOrDefault(node, Collections.emptyList()); + for (int link : links) { + // Defect: linear scan on growing list + if (!objects.contains(link)) { + objects.add(link); + getLinkedRecursiveHelper(graph, link, objects); + } + } + } + + /** Fixed: HashSet for O(1) dedup */ + static List getLinkedRecursiveFixed(Map> graph, int start) { + List objects = new ArrayList<>(); + Set seen = new HashSet<>(); + getLinkedRecursiveFixedHelper(graph, start, objects, seen); + return objects; + } + + static void getLinkedRecursiveFixedHelper(Map> graph, int node, + List objects, Set seen) { + List links = graph.getOrDefault(node, Collections.emptyList()); + for (int link : links) { + if (seen.add(link)) { + objects.add(link); + getLinkedRecursiveFixedHelper(graph, link, objects, seen); + } + } + } + + static boolean testGetLinkedRecursive() { + // Build a graph where node 0 links to 1..N, node 1 links to 2..N, etc. + int N = 2000; + Map> graph = new HashMap<>(); + for (int i = 0; i < N; i++) { + List links = new ArrayList<>(); + for (int j = i + 1; j < Math.min(i + 4, N); j++) { + links.add(j); + } + graph.put(i, links); + } + + // Warmup + for (int i = 0; i < 3; i++) { + getLinkedRecursiveDefective(graph, 0); + getLinkedRecursiveFixed(graph, 0); + } + + long t0 = System.nanoTime(); + for (int i = 0; i < 10; i++) getLinkedRecursiveDefective(graph, 0); + long defective = System.nanoTime() - t0; + + t0 = System.nanoTime(); + for (int i = 0; i < 10; i++) getLinkedRecursiveFixed(graph, 0); + long fixed = System.nanoTime() - t0; + + double ratio = (double) defective / fixed; + System.out.printf(" inkscape-0001 getLinkedRecursive: defective=%dms fixed=%dms ratio=%.1fx%n", + defective / 1_000_000, fixed / 1_000_000, ratio); + return ratio > 2.0; + } + + // ========== inkscape-0002: raise()/lower() ========== + + /** Defective: std::find on items_copy for each sibling */ + static int raiseDefective(List selected, List allSiblings) { + int ops = 0; + for (int child : selected) { + for (int sibling : allSiblings) { + if (sibling == child) continue; + // Defect: linear scan on selected list + if (!selected.contains(sibling)) { + ops++; + break; + } + } + } + return ops; + } + + /** Fixed: HashSet for O(1) membership */ + static int raiseFixed(List selected, List allSiblings) { + Set selectedSet = new HashSet<>(selected); + int ops = 0; + for (int child : selected) { + for (int sibling : allSiblings) { + if (sibling == child) continue; + if (!selectedSet.contains(sibling)) { + ops++; + break; + } + } + } + return ops; + } + + static boolean testRaiseLower() { + int S = 1000; // selected objects + int N = 2000; // total siblings + List selected = new ArrayList<>(); + for (int i = 0; i < S; i++) selected.add(i * 2); + List allSiblings = new ArrayList<>(); + for (int i = 0; i < N; i++) allSiblings.add(i); + + // Warmup + for (int i = 0; i < 3; i++) { + raiseDefective(selected, allSiblings); + raiseFixed(selected, allSiblings); + } + + long t0 = System.nanoTime(); + for (int i = 0; i < 50; i++) raiseDefective(selected, allSiblings); + long defective = System.nanoTime() - t0; + + t0 = System.nanoTime(); + for (int i = 0; i < 50; i++) raiseFixed(selected, allSiblings); + long fixed = System.nanoTime() - t0; + + double ratio = (double) defective / fixed; + System.out.printf(" inkscape-0002 raise/lower: defective=%dms fixed=%dms ratio=%.1fx%n", + defective / 1_000_000, fixed / 1_000_000, ratio); + return ratio > 2.0; + } + + // ========== inkscape-0003: get_all_items_recursive exclude ========== + + /** Defective: std::find on exclude vector per child */ + static List getAllItemsDefective(List allChildren, List exclude) { + List result = new ArrayList<>(); + for (int child : allChildren) { + if (exclude.isEmpty() || !exclude.contains(child)) { + result.add(child); + } + } + return result; + } + + /** Fixed: unordered_set for O(1) exclusion check */ + static List getAllItemsFixed(List allChildren, List exclude) { + Set excludeSet = new HashSet<>(exclude); + List result = new ArrayList<>(); + for (int child : allChildren) { + if (excludeSet.isEmpty() || !excludeSet.contains(child)) { + result.add(child); + } + } + return result; + } + + static boolean testGetAllItemsExclude() { + int C = 2000; // children in document + int E = 1000; // excluded (current selection for invert) + List allChildren = new ArrayList<>(); + for (int i = 0; i < C; i++) allChildren.add(i); + List exclude = new ArrayList<>(); + for (int i = 0; i < E; i++) exclude.add(i * 2); + + // Warmup + for (int i = 0; i < 3; i++) { + getAllItemsDefective(allChildren, exclude); + getAllItemsFixed(allChildren, exclude); + } + + long t0 = System.nanoTime(); + for (int i = 0; i < 100; i++) getAllItemsDefective(allChildren, exclude); + long defective = System.nanoTime() - t0; + + t0 = System.nanoTime(); + for (int i = 0; i < 100; i++) getAllItemsFixed(allChildren, exclude); + long fixed = System.nanoTime() - t0; + + double ratio = (double) defective / fixed; + System.out.printf(" inkscape-0003 getAllItems exclude: defective=%dms fixed=%dms ratio=%.1fx%n", + defective / 1_000_000, fixed / 1_000_000, ratio); + return ratio > 2.0; + } + + // ========== Main ========== + + public static void main(String[] args) { + System.out.println("Inkscape CWE-407 unit tests"); + System.out.println("=========================="); + + boolean p1 = testGetLinkedRecursive(); + boolean p2 = testRaiseLower(); + boolean p3 = testGetAllItemsExclude(); + + System.out.println(); + System.out.printf("inkscape-0001 getLinkedRecursive: %s%n", p1 ? "PASS" : "FAIL"); + System.out.printf("inkscape-0002 raise/lower: %s%n", p2 ? "PASS" : "FAIL"); + System.out.printf("inkscape-0003 getAllItems exclude: %s%n", p3 ? "PASS" : "FAIL"); + + if (!p1 || !p2 || !p3) { + System.exit(1); + } + System.out.println("\nAll 3 tests PASS"); + } +}