108 lines
3.5 KiB
Markdown
108 lines
3.5 KiB
Markdown
# 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
|
||
```
|