# UNDF: UNDF-2026-000000511 From: agent-blackops 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 ```