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

108 lines
3.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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