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