wave8a: 436/195 — istio-0002, spark-0002, igraph-0001 + PDF
This commit is contained in:
parent
5fe6da7cc2
commit
2d3e3d603e
7 changed files with 508 additions and 5 deletions
107
defects/python-igraph/patch/igraph-0001-max-cohesion-set.md
Normal file
107
defects/python-igraph/patch/igraph-0001-max-cohesion-set.md
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
From: agent-blackops <blackops@unturf.com>
|
||||
Date: Fri, 27 Mar 2026 00:00:00 +0000
|
||||
Subject: [PATCH] clustering: replace list membership scan in CohesiveBlocks.max_cohesion with set lookup
|
||||
|
||||
CWE-407: O(N) list membership test inside a loop in CohesiveBlocks.max_cohesion().
|
||||
|
||||
## Defect
|
||||
|
||||
File: `src/igraph/clustering.py`
|
||||
Lines: 1305-1313 (`max_cohesion`), list membership test at line 1311
|
||||
|
||||
`CohesiveBlocks` inherits from `Cover`, which initialises `self._clusters`
|
||||
as a list of lists (line 968):
|
||||
|
||||
```python
|
||||
self._clusters = [list(cluster) for cluster in clusters]
|
||||
```
|
||||
|
||||
`max_cohesion(idx)` loops over every block and checks whether vertex `idx`
|
||||
is in that block:
|
||||
|
||||
```python
|
||||
def max_cohesion(self, idx): # line 1305
|
||||
result = 0
|
||||
for cohesion, cluster in zip(self._cohesion, self._clusters): # O(B) outer
|
||||
if idx in cluster: # line 1311 ← O(|cluster|) list scan
|
||||
result = max(result, cohesion)
|
||||
return result
|
||||
```
|
||||
|
||||
Cost per call: O(B × C) where B = number of cohesive blocks, C = average
|
||||
cluster size. When a caller queries all V vertices in a loop (the natural
|
||||
use — "colour each vertex by its max cohesion"), total cost becomes
|
||||
O(V × B × C). For dense graphs B × C can reach O(V), making the full
|
||||
pass O(V²).
|
||||
|
||||
## Complexity
|
||||
|
||||
- Outer loop: O(B) — iterates every cohesive block
|
||||
- Inner `in` test on list: O(|cluster|) average — linear scan
|
||||
- Per-call total: O(B × C)
|
||||
- Full V-vertex pass: O(V × B × C) → O(V²) in the worst case
|
||||
|
||||
## Fix
|
||||
|
||||
At class initialisation time, build a parallel `_cluster_sets` index that
|
||||
maps each cluster to a `frozenset` for O(1) membership testing:
|
||||
|
||||
```python
|
||||
# In Cover.__init__ (src/igraph/clustering.py, after line 968)
|
||||
self._cluster_sets = [frozenset(c) for c in self._clusters]
|
||||
```
|
||||
|
||||
Then replace the linear scan:
|
||||
|
||||
```python
|
||||
def max_cohesion(self, idx):
|
||||
result = 0
|
||||
for cohesion, cluster_set in zip(self._cohesion, self._cluster_sets):
|
||||
if idx in cluster_set: # O(1) hash lookup — CWE-407 fix
|
||||
result = max(result, cohesion)
|
||||
return result
|
||||
```
|
||||
|
||||
Full-pass cost drops from O(V × B × C) → O(V × B).
|
||||
|
||||
## Severity
|
||||
|
||||
MEDIUM — `max_cohesion` is a documented public API method on `CohesiveBlocks`.
|
||||
Users are expected to call it once per vertex to colour or rank vertices.
|
||||
A V-vertex graph with O(V) cohesive blocks of average size O(V) yields O(V²)
|
||||
work per full pass. In practice cohesive block counts and sizes are modest
|
||||
for sparse graphs, but the API contract invites unbounded use.
|
||||
|
||||
## Defect-Id
|
||||
|
||||
igraph-0001
|
||||
|
||||
## CWE
|
||||
|
||||
CWE-407 (Inefficient Algorithmic Complexity)
|
||||
|
||||
---
|
||||
## Diff
|
||||
|
||||
```diff
|
||||
--- a/src/igraph/clustering.py
|
||||
+++ b/src/igraph/clustering.py
|
||||
@@ -968,6 +968,7 @@ class Cover:
|
||||
self._clusters = [list(cluster) for cluster in clusters]
|
||||
+ self._cluster_sets = [frozenset(c) for c in self._clusters]
|
||||
try:
|
||||
self._n = max(max(cluster) + 1 for cluster in self._clusters if cluster)
|
||||
except ValueError:
|
||||
|
||||
@@ -1305,9 +1306,9 @@ class CohesiveBlocks(VertexCover):
|
||||
def max_cohesion(self, idx):
|
||||
"""Finds the maximum cohesion score among all the groups that contain
|
||||
the given vertex."""
|
||||
result = 0
|
||||
- for cohesion, cluster in zip(self._cohesion, self._clusters):
|
||||
- if idx in cluster: # CWE-407: O(|cluster|) list scan
|
||||
+ for cohesion, cluster_set in zip(self._cohesion, self._cluster_sets):
|
||||
+ if idx in cluster_set: # CWE-407 fix: O(1) frozenset lookup
|
||||
result = max(result, cohesion)
|
||||
return result
|
||||
```
|
||||
187
defects/python-igraph/unit/IgraphCohesiveBlocksTest.java
Normal file
187
defects/python-igraph/unit/IgraphCohesiveBlocksTest.java
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
package unit;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* IgraphCohesiveBlocksTest
|
||||
*
|
||||
* Models CWE-407 defect igraph-0001:
|
||||
*
|
||||
* python-igraph CohesiveBlocks.max_cohesion() — list membership scan
|
||||
* File: src/igraph/clustering.py, line 1311
|
||||
*
|
||||
* Defective: self._clusters is a list-of-lists; `if idx in cluster` is
|
||||
* O(|cluster|) per block, O(B×C) per call, O(V×B×C) for all
|
||||
* vertices — degrades to O(V²) when B×C ~ V.
|
||||
*
|
||||
* Fixed: self._cluster_sets is a list-of-frozensets; `if idx in
|
||||
* cluster_set` is O(1); full-pass cost is O(V×B).
|
||||
*
|
||||
* Tests instrument comparison counts explicitly — no wall-clock timing.
|
||||
*/
|
||||
public class IgraphCohesiveBlocksTest {
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Model: a CohesiveBlocks object with B blocks, each of size C.
|
||||
// Vertex indices are integers 0..V-1.
|
||||
// We simulate max_cohesion(idx) for each vertex 0..V-1.
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Defective path: each cluster stored as ArrayList.
|
||||
* `if idx in cluster` performs a linear scan of the list.
|
||||
*
|
||||
* @param numVertices V — total vertex count
|
||||
* @param numBlocks B — number of cohesive blocks
|
||||
* @param clusterSize C — vertices per block (each block = consecutive V IDs)
|
||||
* @return total element comparisons across all V calls to max_cohesion
|
||||
*/
|
||||
static long defectiveMaxCohesion(int numVertices, int numBlocks, int clusterSize) {
|
||||
// Build B clusters, each containing clusterSize consecutive vertex IDs
|
||||
// (wrapping mod V to keep IDs in range)
|
||||
List<ArrayList<Integer>> clusters = new ArrayList<>();
|
||||
int[] cohesion = new int[numBlocks];
|
||||
for (int b = 0; b < numBlocks; b++) {
|
||||
ArrayList<Integer> cluster = new ArrayList<>();
|
||||
for (int c = 0; c < clusterSize; c++) {
|
||||
cluster.add((b * clusterSize + c) % numVertices);
|
||||
}
|
||||
clusters.add(cluster);
|
||||
cohesion[b] = b + 1; // arbitrary cohesion score
|
||||
}
|
||||
|
||||
long comparisons = 0;
|
||||
// Simulate max_cohesion(idx) for every vertex (the natural full-pass use)
|
||||
for (int idx = 0; idx < numVertices; idx++) {
|
||||
// Defective: for each block, scan the list linearly
|
||||
for (int b = 0; b < numBlocks; b++) {
|
||||
ArrayList<Integer> cluster = clusters.get(b);
|
||||
for (Integer member : cluster) {
|
||||
comparisons++; // O(|cluster|) list scan
|
||||
if (member.equals(idx)) {
|
||||
break; // found — stop scanning this cluster
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return comparisons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixed path: each cluster stored as HashSet (models Python frozenset).
|
||||
* `if idx in cluster_set` is O(1) average.
|
||||
*
|
||||
* @param numVertices V
|
||||
* @param numBlocks B
|
||||
* @param clusterSize C
|
||||
* @return total hash lookups across all V calls to max_cohesion
|
||||
*/
|
||||
static long fixedMaxCohesion(int numVertices, int numBlocks, int clusterSize) {
|
||||
List<HashSet<Integer>> clusterSets = new ArrayList<>();
|
||||
for (int b = 0; b < numBlocks; b++) {
|
||||
HashSet<Integer> set = new HashSet<>();
|
||||
for (int c = 0; c < clusterSize; c++) {
|
||||
set.add((b * clusterSize + c) % numVertices);
|
||||
}
|
||||
clusterSets.add(set);
|
||||
}
|
||||
|
||||
long lookups = 0;
|
||||
for (int idx = 0; idx < numVertices; idx++) {
|
||||
for (int b = 0; b < numBlocks; b++) {
|
||||
lookups++; // O(1) hash probe — CWE-407 fix
|
||||
clusterSets.get(b).contains(idx);
|
||||
}
|
||||
}
|
||||
return lookups;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Test 1: defective cost > fixed cost at V=100, B=20, C=10
|
||||
// -----------------------------------------------------------------------
|
||||
static void test1_listScanCostsMoreThanSetLookup() {
|
||||
int V = 100, B = 20, C = 10;
|
||||
long defectOps = defectiveMaxCohesion(V, B, C);
|
||||
long fixedOps = fixedMaxCohesion(V, B, C);
|
||||
|
||||
System.out.printf(
|
||||
"test1: V=%d B=%d C=%d defect_comparisons=%d fixed_lookups=%d%n",
|
||||
V, B, C, defectOps, fixedOps);
|
||||
|
||||
assert defectOps > fixedOps
|
||||
: "igraph-0001: list scan must do more work than set lookup; defect="
|
||||
+ defectOps + " fixed=" + fixedOps;
|
||||
// Fixed cost is exactly V*B (one hash probe per block per vertex)
|
||||
assert fixedOps == (long) V * B
|
||||
: "igraph-0001: fixed lookups should be V*B=" + ((long) V * B)
|
||||
+ " got " + fixedOps;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Test 2: defect grows super-linearly with clusterSize; fixed does not
|
||||
// Doubling C doubles defect cost (more comparisons per scan),
|
||||
// but fixed cost stays constant (O(1) per lookup regardless of C).
|
||||
// -----------------------------------------------------------------------
|
||||
static void test2_defectGrowsWithClusterSize() {
|
||||
int V = 80, B = 10;
|
||||
int C1 = 8, C2 = 16; // double cluster size
|
||||
|
||||
long d1 = defectiveMaxCohesion(V, B, C1);
|
||||
long d2 = defectiveMaxCohesion(V, B, C2);
|
||||
long f1 = fixedMaxCohesion(V, B, C1);
|
||||
long f2 = fixedMaxCohesion(V, B, C2);
|
||||
|
||||
System.out.printf(
|
||||
"test2: V=%d B=%d C1=%d defect=%d fixed=%d | C2=%d defect=%d fixed=%d%n",
|
||||
V, B, C1, d1, f1, C2, d2, f2);
|
||||
|
||||
// Defect cost grows with C; fixed cost is independent of C
|
||||
assert d2 > d1
|
||||
: "igraph-0001: defect comparisons must grow as cluster size increases";
|
||||
assert f2 == f1
|
||||
: "igraph-0001: fixed lookups must not change with cluster size; f1="
|
||||
+ f1 + " f2=" + f2;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Test 3: at large scale (V=500, B=50, C=50) defect cost is at least
|
||||
// 10x the fixed cost — demonstrating quadratic vs linear behaviour.
|
||||
// -----------------------------------------------------------------------
|
||||
static void test3_largeScaleSpeedup() {
|
||||
int V = 500, B = 50, C = 50;
|
||||
long defectOps = defectiveMaxCohesion(V, B, C);
|
||||
long fixedOps = fixedMaxCohesion(V, B, C);
|
||||
|
||||
double ratio = (double) defectOps / Math.max(1, fixedOps);
|
||||
|
||||
System.out.printf(
|
||||
"test3: V=%d B=%d C=%d defect=%d fixed=%d ratio=%.1fx%n",
|
||||
V, B, C, defectOps, fixedOps, ratio);
|
||||
|
||||
assert ratio >= 10.0
|
||||
: "igraph-0001: expected >= 10x speedup from set; got ratio=" + ratio;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Main
|
||||
// -----------------------------------------------------------------------
|
||||
public static void main(String[] args) {
|
||||
System.out.println("=== IgraphCohesiveBlocksTest ===");
|
||||
System.out.println("Modelling CWE-407: igraph-0001 — CohesiveBlocks.max_cohesion list scan");
|
||||
System.out.println();
|
||||
|
||||
test1_listScanCostsMoreThanSetLookup();
|
||||
System.out.println(" PASS test1_listScanCostsMoreThanSetLookup");
|
||||
|
||||
test2_defectGrowsWithClusterSize();
|
||||
System.out.println(" PASS test2_defectGrowsWithClusterSize");
|
||||
|
||||
test3_largeScaleSpeedup();
|
||||
System.out.println(" PASS test3_largeScaleSpeedup");
|
||||
|
||||
System.out.println();
|
||||
System.out.println("3/3 PASS");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue