undefect. CWE-407 — 92 sites, 42 ecosystems
B&W print-friendly diagrams + tinkerpop-0001 + wave-3 proof sections. Squash of 94 local commits onto remote master.
This commit is contained in:
parent
0a580b313d
commit
db29a08762
1311 changed files with 371202 additions and 1188 deletions
|
|
@ -0,0 +1,44 @@
|
|||
From: agent-blackops <blackops@unturf.com>
|
||||
Date: Thu, 26 Mar 2026 00:00:00 +0000
|
||||
Subject: [PATCH] algorithms/cycles: replace B defaultdict(list) with defaultdict(set) in recursive_simple_cycles
|
||||
|
||||
CWE-407: Algorithmic complexity via O(N) list membership test in
|
||||
recursive_simple_cycles(). B was a defaultdict(list) used to track
|
||||
graph portions yielding no elementary circuit. The inner loop called
|
||||
`if thisnode not in B[nextnode]` (O(|B[nextnode]|)) followed by
|
||||
`B[nextnode].append(thisnode)` inside circuit(), which is invoked for
|
||||
every edge in every DFS frame. Total cost per component is O(E × |B|).
|
||||
|
||||
Replace with defaultdict(set): `not in` on a set is O(1) amortised;
|
||||
`add` replaces `append`. The `_unblock` helper uses `pop()` on the
|
||||
collection — set.pop() is valid and semantically equivalent here since
|
||||
order does not matter for unblocking. No algorithmic contract changes.
|
||||
|
||||
Defect-Id: NX-001
|
||||
Severity: MEDIUM
|
||||
CWE: CWE-407 (Inefficient Algorithmic Complexity)
|
||||
---
|
||||
networkx/algorithms/cycles.py | 6 +++---
|
||||
1 file changed, 3 insertions(+), 3 deletions(-)
|
||||
|
||||
diff --git a/networkx/algorithms/cycles.py b/networkx/algorithms/cycles.py
|
||||
index xxxxxxx..yyyyyyy 100644
|
||||
--- a/networkx/algorithms/cycles.py
|
||||
+++ b/networkx/algorithms/cycles.py
|
||||
@@ -840,11 +840,11 @@ def recursive_simple_cycles(G):
|
||||
if closed:
|
||||
_unblock(thisnode)
|
||||
else:
|
||||
for nextnode in component[thisnode]:
|
||||
- if thisnode not in B[nextnode]: # TODO: use set for speedup?
|
||||
- B[nextnode].append(thisnode)
|
||||
+ if thisnode not in B[nextnode]: # CWE-407 fix: O(1) set lookup
|
||||
+ B[nextnode].add(thisnode) # CWE-407 fix: O(1) set insert
|
||||
path.pop() # remove thisnode from path
|
||||
return closed
|
||||
|
||||
path = [] # stack of nodes in current path
|
||||
blocked = defaultdict(bool) # vertex: blocked from search?
|
||||
- B = defaultdict(list) # graph portions that yield no elementary circuit
|
||||
+ B = defaultdict(set) # CWE-407 fix: set for O(1) membership and insert
|
||||
result = [] # list to accumulate the circuits found
|
||||
390
defects/networkx/unit/NetworkXCyclesTest.java
Normal file
390
defects/networkx/unit/NetworkXCyclesTest.java
Normal file
|
|
@ -0,0 +1,390 @@
|
|||
package unit;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.IdentityHashMap;
|
||||
|
||||
/**
|
||||
* NetworkXCyclesTest
|
||||
*
|
||||
* Models the CWE-407 defects across three projects:
|
||||
*
|
||||
* NX-001 (MEDIUM): networkx recursive_simple_cycles — B[nextnode] dedup
|
||||
* Defective: B[nextnode] is an ArrayList; `not in` is O(|B[nextnode]|) per edge.
|
||||
* Fixed: B[nextnode] is a HashSet; `not in` (contains) is O(1).
|
||||
*
|
||||
* rubocop-0001 (MEDIUM): RuboCop IgnoredNode — @ignored_nodes dedup
|
||||
* Defective: @ignored_nodes is an ArrayList; identity scan O(N) per on_str call.
|
||||
* Fixed: @ignored_nodes is a HashSet with identity-based hashing; O(1).
|
||||
*
|
||||
* solargraph-0001 (MEDIUM): Solargraph Chain — @@inference_stack dedup
|
||||
* Defective: @@inference_stack is a shared ArrayList; O(D) include? per pin
|
||||
* AND shared across simulated concurrent "threads" (no isolation).
|
||||
* Fixed: Per-thread HashSet; O(1) contains, isolated per thread.
|
||||
*
|
||||
* All tests instrument operation counts explicitly — no wall-clock timing.
|
||||
*/
|
||||
public class NetworkXCyclesTest {
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// NX-001: B[nextnode] deduplication model
|
||||
// Simulates the inner loop of circuit() across E edges and growing B sets.
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Defective: B[nextnode] is ArrayList; `not in` is O(|B[nextnode]|).
|
||||
*
|
||||
* Edge pattern: all edges target the same nextnode=0, with thisnode running
|
||||
* from 0..numUniqueThisNodes-1. This maximises B[0] growth: after k distinct
|
||||
* thisnode values have been added, the next novel thisnode scans all k entries
|
||||
* before discovering it is absent. Total comparisons = 0+1+2+...+(k-1) = k*(k-1)/2.
|
||||
* Subsequent repeats of already-seen thisnode values each scan until they hit,
|
||||
* averaging k/2 comparisons per repeat.
|
||||
*/
|
||||
static long nx001_defectiveBDedup(int numDistinctSources, int numRepeats) {
|
||||
ArrayList<Integer> B0 = new ArrayList<>(); // B[nextnode=0]
|
||||
long comparisons = 0;
|
||||
// Phase 1: insert all distinct thisnode values 0..numDistinctSources-1
|
||||
for (int thisnode = 0; thisnode < numDistinctSources; thisnode++) {
|
||||
// Scan full list before each new insert — O(size) miss
|
||||
for (Integer existing : B0) {
|
||||
comparisons++;
|
||||
// won't match — thisnode not yet in list
|
||||
}
|
||||
B0.add(thisnode);
|
||||
}
|
||||
// Phase 2: repeat lookups for already-present nodes — O(position) hit
|
||||
for (int rep = 0; rep < numRepeats; rep++) {
|
||||
int thisnode = rep % numDistinctSources;
|
||||
for (Integer existing : B0) {
|
||||
comparisons++;
|
||||
if (existing.equals(thisnode)) break; // hit at position thisnode
|
||||
}
|
||||
}
|
||||
return comparisons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixed: B[nextnode] is HashSet; `not in` (contains) is O(1).
|
||||
* Same edge pattern — all operations are one hash probe each.
|
||||
*/
|
||||
static long nx001_fixedBDedup(int numDistinctSources, int numRepeats) {
|
||||
HashSet<Integer> B0 = new HashSet<>();
|
||||
long lookups = 0;
|
||||
// Phase 1: insert distinct sources — O(1) contains check each
|
||||
for (int thisnode = 0; thisnode < numDistinctSources; thisnode++) {
|
||||
lookups++; // one O(1) hash probe — CWE-407 fix
|
||||
B0.add(thisnode);
|
||||
}
|
||||
// Phase 2: repeat lookups — O(1) each
|
||||
for (int rep = 0; rep < numRepeats; rep++) {
|
||||
lookups++; // one O(1) hash probe — CWE-407 fix
|
||||
}
|
||||
return lookups;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// rubocop-0001: @ignored_nodes identity dedup model
|
||||
// Simulates on_str firing R times with S ignored nodes accumulated.
|
||||
// Nodes are modelled as Long object IDs (identity comparison via ==).
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/** Defective: ignored_nodes is ArrayList; identity scan O(S) per on_str call. */
|
||||
static long rubocop0001_defectiveIgnoredNodes(int numStringNodes, int numIgnoredNodes) {
|
||||
// Build ignored_nodes list (S entries)
|
||||
ArrayList<Long> ignoredNodes = new ArrayList<>();
|
||||
Long[] nodeObjects = new Long[numIgnoredNodes];
|
||||
for (int i = 0; i < numIgnoredNodes; i++) {
|
||||
nodeObjects[i] = (long) (i + 1_000_000); // distinct Long objects
|
||||
ignoredNodes.add(nodeObjects[i]);
|
||||
}
|
||||
long comparisons = 0;
|
||||
// Simulate on_str called R times — each call checks ignored_node?(node)
|
||||
for (int r = 0; r < numStringNodes; r++) {
|
||||
Long queryNode = nodeObjects[r % numIgnoredNodes]; // always a hit
|
||||
// `ignored_nodes.any? { |n| n.equal?(node) }` — O(S) scan
|
||||
for (Long ignored : ignoredNodes) {
|
||||
comparisons++;
|
||||
if (ignored == queryNode) { // identity comparison
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return comparisons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixed: ignored_nodes is an identity-based HashSet; include?(node) is O(1).
|
||||
* Java models identity-based hashing via IdentityHashMap used as a Set.
|
||||
*/
|
||||
static long rubocop0001_fixedIgnoredNodes(int numStringNodes, int numIgnoredNodes) {
|
||||
// IdentityHashMap with dummy values models Set.new.compare_by_identity
|
||||
IdentityHashMap<Long, Boolean> ignoredNodes = new IdentityHashMap<>();
|
||||
Long[] nodeObjects = new Long[numIgnoredNodes];
|
||||
for (int i = 0; i < numIgnoredNodes; i++) {
|
||||
nodeObjects[i] = (long) (i + 1_000_000);
|
||||
ignoredNodes.put(nodeObjects[i], Boolean.TRUE);
|
||||
}
|
||||
long lookups = 0;
|
||||
for (int r = 0; r < numStringNodes; r++) {
|
||||
Long queryNode = nodeObjects[r % numIgnoredNodes];
|
||||
lookups++; // one O(1) identity hash lookup — CWE-407 fix
|
||||
ignoredNodes.containsKey(queryNode);
|
||||
}
|
||||
return lookups;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// solargraph-0001: @@inference_stack isolation + dedup model
|
||||
// Simulates infer_from_definitions across T concurrent "threads",
|
||||
// each with D pins to process at inference depth D.
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/** Shared mutable state modelling @@inference_stack = [] (the defect). */
|
||||
static class DefectiveInferenceStack {
|
||||
final ArrayList<Long> stack = new ArrayList<>();
|
||||
long comparisons = 0;
|
||||
|
||||
boolean include(long pinId) {
|
||||
for (Long existing : stack) {
|
||||
comparisons++;
|
||||
if (existing.equals(pinId)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
void push(long pinId) { stack.add(pinId); }
|
||||
void pop() { if (!stack.isEmpty()) stack.remove(stack.size() - 1); }
|
||||
}
|
||||
|
||||
/** Per-thread state modelling Thread.current[:solargraph_inference_stack] (the fix). */
|
||||
static class FixedInferenceStack {
|
||||
// Each "thread" gets its own HashSet — thread-local isolation
|
||||
final HashMap<Integer, HashSet<Long>> threadStacks = new HashMap<>();
|
||||
long lookups = 0;
|
||||
|
||||
private HashSet<Long> stackFor(int threadId) {
|
||||
return threadStacks.computeIfAbsent(threadId, k -> new HashSet<>());
|
||||
}
|
||||
|
||||
boolean include(int threadId, long pinId) {
|
||||
lookups++; // O(1) hash lookup — CWE-407 fix
|
||||
return stackFor(threadId).contains(pinId);
|
||||
}
|
||||
void add(int threadId, long pinId) { stackFor(threadId).add(pinId); }
|
||||
void delete(int threadId, long pinId) { stackFor(threadId).remove(pinId); }
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate T threads each processing D pins through the defective shared stack.
|
||||
* Returns total comparisons across all threads.
|
||||
*/
|
||||
static long solargraph0001_defectiveStack(int numThreads, int pinsPerThread) {
|
||||
DefectiveInferenceStack shared = new DefectiveInferenceStack();
|
||||
// Sequential simulation: each thread pushes its pins, checks, pops
|
||||
for (int t = 0; t < numThreads; t++) {
|
||||
for (int p = 0; p < pinsPerThread; p++) {
|
||||
long pinId = (long) t * pinsPerThread + p;
|
||||
// `next if @@inference_stack.include?(pin)` — O(D) scan
|
||||
shared.include(pinId);
|
||||
shared.push(pinId);
|
||||
}
|
||||
// pop all pins for this "thread" (in defective impl they share the stack)
|
||||
for (int p = 0; p < pinsPerThread; p++) {
|
||||
shared.pop();
|
||||
}
|
||||
}
|
||||
return shared.comparisons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate T threads each processing D pins through the fixed per-thread Set.
|
||||
* Returns total lookups across all threads.
|
||||
*/
|
||||
static long solargraph0001_fixedStack(int numThreads, int pinsPerThread) {
|
||||
FixedInferenceStack fixed = new FixedInferenceStack();
|
||||
for (int t = 0; t < numThreads; t++) {
|
||||
for (int p = 0; p < pinsPerThread; p++) {
|
||||
long pinId = (long) t * pinsPerThread + p;
|
||||
// O(1) set lookup — CWE-407 fix
|
||||
fixed.include(t, pinId);
|
||||
fixed.add(t, pinId);
|
||||
}
|
||||
for (int p = 0; p < pinsPerThread; p++) {
|
||||
long pinId = (long) t * pinsPerThread + p;
|
||||
fixed.delete(t, pinId);
|
||||
}
|
||||
}
|
||||
return fixed.lookups;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Test 1 — NX-001: defective B-list scan > fixed B-set lookup at E=200, N=20
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
static void test1_nx001_BSetVsList() {
|
||||
int numDistinctSources = 50;
|
||||
int numRepeats = 100;
|
||||
long defectOps = nx001_defectiveBDedup(numDistinctSources, numRepeats);
|
||||
long fixedOps = nx001_fixedBDedup(numDistinctSources, numRepeats);
|
||||
|
||||
// Phase-1 defect cost: triangular 0+1+...+(k-1) = k*(k-1)/2
|
||||
long expectedPhase1Defect = (long) numDistinctSources * (numDistinctSources - 1) / 2;
|
||||
|
||||
System.out.printf(
|
||||
"test1 NX-001: distinct_sources=%d repeats=%d defect_comparisons=%d (expect phase1>=%d) fixed_lookups=%d%n",
|
||||
numDistinctSources, numRepeats, defectOps, expectedPhase1Defect, fixedOps);
|
||||
|
||||
assert defectOps > fixedOps
|
||||
: "NX-001: defective list scan must do more comparisons than set lookup; defect="
|
||||
+ defectOps + " fixed=" + fixedOps;
|
||||
assert defectOps >= expectedPhase1Defect
|
||||
: "NX-001: defect comparisons=" + defectOps + " must be at least triangular=" + expectedPhase1Defect;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Test 2 — NX-001: scaling — doubling edges grows defect super-linearly
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
static void test2_nx001_quadraticScaling() {
|
||||
// Double the number of distinct sources; repeats held constant.
|
||||
// Defect phase-1 cost is k*(k-1)/2 — quadratic in k.
|
||||
// Fixed cost is k + repeats — linear in k.
|
||||
int repeats = 50;
|
||||
int k1 = 40;
|
||||
int k2 = 80; // 2x k
|
||||
|
||||
long d1 = nx001_defectiveBDedup(k1, repeats);
|
||||
long d2 = nx001_defectiveBDedup(k2, repeats);
|
||||
long f1 = nx001_fixedBDedup(k1, repeats);
|
||||
long f2 = nx001_fixedBDedup(k2, repeats);
|
||||
|
||||
double defectGrowth = (double) d2 / Math.max(1, d1);
|
||||
double fixedGrowth = (double) f2 / Math.max(1, f1);
|
||||
|
||||
System.out.printf(
|
||||
"test2 NX-001 scaling: k1=%d k2=%d defect_growth=%.2fx fixed_growth=%.2fx%n",
|
||||
k1, k2, defectGrowth, fixedGrowth);
|
||||
|
||||
assert defectGrowth > fixedGrowth
|
||||
: "NX-001: defect should grow faster than fix when k doubles; got defect="
|
||||
+ defectGrowth + " fixed=" + fixedGrowth;
|
||||
assert defectGrowth > 2.0
|
||||
: "NX-001: defect should grow super-linearly (quadratic), got " + defectGrowth;
|
||||
assert fixedGrowth <= 2.5
|
||||
: "NX-001: fixed set lookup should grow at most linearly; got " + fixedGrowth;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Test 3 — rubocop-0001: identity-list scan > identity-set lookup at R=500, S=50
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
static void test3_rubocop0001_identitySetVsList() {
|
||||
int numStringNodes = 500;
|
||||
int numIgnoredNodes = 50;
|
||||
|
||||
long defectOps = rubocop0001_defectiveIgnoredNodes(numStringNodes, numIgnoredNodes);
|
||||
long fixedOps = rubocop0001_fixedIgnoredNodes(numStringNodes, numIgnoredNodes);
|
||||
|
||||
System.out.printf(
|
||||
"test3 rubocop-0001: on_str=%d ignored=%d defect_comparisons=%d fixed_lookups=%d%n",
|
||||
numStringNodes, numIgnoredNodes, defectOps, fixedOps);
|
||||
|
||||
assert defectOps > fixedOps
|
||||
: "rubocop-0001: identity list scan must cost more than identity set lookup";
|
||||
// Worst case for defect: query always hits last element → S comparisons each
|
||||
// With hits cycling through all S nodes (always hits at position r%S+1 on average),
|
||||
// total should be at least R comparisons.
|
||||
assert defectOps >= numStringNodes
|
||||
: "rubocop-0001: expected at least R=" + numStringNodes + " comparisons, got " + defectOps;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Test 4 — solargraph-0001: shared-stack list scan > per-thread set lookup
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
static void test4_solargraph0001_threadLocalSetVsSharedList() {
|
||||
int numThreads = 10;
|
||||
int pinsPerThread = 30;
|
||||
|
||||
long defectOps = solargraph0001_defectiveStack(numThreads, pinsPerThread);
|
||||
long fixedOps = solargraph0001_fixedStack(numThreads, pinsPerThread);
|
||||
|
||||
System.out.printf(
|
||||
"test4 solargraph-0001: threads=%d pins_per_thread=%d defect_comparisons=%d fixed_lookups=%d%n",
|
||||
numThreads, pinsPerThread, defectOps, fixedOps);
|
||||
|
||||
assert defectOps > fixedOps
|
||||
: "solargraph-0001: shared-list scan must do more work than per-thread set";
|
||||
assert defectOps > 0
|
||||
: "solargraph-0001: defect must perform at least one comparison";
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Test 5 — solargraph-0001: thread isolation — per-thread set never sees
|
||||
// another thread's pins (no cross-contamination in fixed impl)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
static void test5_solargraph0001_perThreadIsolation() {
|
||||
FixedInferenceStack fixed = new FixedInferenceStack();
|
||||
int numThreads = 5;
|
||||
int pinsPerThread = 20;
|
||||
|
||||
// Each thread adds its own pins
|
||||
for (int t = 0; t < numThreads; t++) {
|
||||
for (int p = 0; p < pinsPerThread; p++) {
|
||||
long pinId = (long) t * pinsPerThread + p;
|
||||
fixed.add(t, pinId);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify thread T cannot see thread T+1's pins (isolation invariant)
|
||||
for (int t = 0; t < numThreads - 1; t++) {
|
||||
long otherThreadPin = (long) (t + 1) * pinsPerThread; // first pin of next thread
|
||||
boolean crossVisible = fixed.stackFor(t).contains(otherThreadPin);
|
||||
assert !crossVisible
|
||||
: "solargraph-0001: thread " + t + " must not see pin from thread " + (t+1)
|
||||
+ " (pin=" + otherThreadPin + ")";
|
||||
}
|
||||
|
||||
// Verify each thread can see its own pins
|
||||
for (int t = 0; t < numThreads; t++) {
|
||||
long ownPin = (long) t * pinsPerThread; // first pin of this thread
|
||||
boolean selfVisible = fixed.stackFor(t).contains(ownPin);
|
||||
assert selfVisible
|
||||
: "solargraph-0001: thread " + t + " must be able to see its own pin (pin=" + ownPin + ")";
|
||||
}
|
||||
|
||||
System.out.printf(
|
||||
"test5 solargraph-0001 isolation: %d threads x %d pins — no cross-contamination confirmed%n",
|
||||
numThreads, pinsPerThread);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Main
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("=== NetworkXCyclesTest ===");
|
||||
System.out.println("Modelling CWE-407: NX-001 + rubocop-0001 + solargraph-0001");
|
||||
System.out.println();
|
||||
|
||||
test1_nx001_BSetVsList();
|
||||
System.out.println(" PASS test1_nx001_BSetVsList");
|
||||
|
||||
test2_nx001_quadraticScaling();
|
||||
System.out.println(" PASS test2_nx001_quadraticScaling");
|
||||
|
||||
test3_rubocop0001_identitySetVsList();
|
||||
System.out.println(" PASS test3_rubocop0001_identitySetVsList");
|
||||
|
||||
test4_solargraph0001_threadLocalSetVsSharedList();
|
||||
System.out.println(" PASS test4_solargraph0001_threadLocalSetVsSharedList");
|
||||
|
||||
test5_solargraph0001_perThreadIsolation();
|
||||
System.out.println(" PASS test5_solargraph0001_perThreadIsolation");
|
||||
|
||||
System.out.println();
|
||||
System.out.println("All 5 tests PASSED.");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue