java-topology/defects/python-igraph/patch/igraph-0001-max-cohesion-set.md

3.5 KiB
Raw Blame History

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):

self._clusters = [list(cluster) for cluster in clusters]

max_cohesion(idx) loops over every block and checks whether vertex idx is in that block:

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:

# 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:

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

--- 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