New defects from diamond O(2^D) sweep: - cpython-0002/0003/0004: pydoc.allmethods, turtle.__methodDict, idlelib.rpc._getmethods - micronaut-0005/0006/0007: populateTypeHierarchy, populateTypeArgumentsForInterfaces, SuperclassAwareTypeVisitor - quarkus-0004/0005: HierarchyDiscovery.discoverTypes, ConfigMappingUtils.collectInterfacesRec - weld-0004/0005: HierarchyDiscovery.discoverTypes, Services.identifyServiceInterfaces - rails-0019: Digestor#dependency_digest Array#include? O(N²) - spring-0007: AnnotationsScanner.processClassHierarchy O(2^D) - django-0007: migrations.state.flatten_bases O(2^D) - typescript-0005: hasBaseType O(2^D) - hibernate-validator-0003: ClassHierarchyHelper.getImplementedInterfaces O(2^D) - swift-0001: QualifiedLookupRequest::evaluate protocol superclass O(2^D)
85 lines
2.6 KiB
Markdown
85 lines
2.6 KiB
Markdown
# UNDF: UNDF-2026-000000673
|
||
# UNDF: (pending)
|
||
# cpython-0002: pydoc.allmethods — O(2^D) diamond base traversal
|
||
|
||
## CWE-407 — Algorithmic Complexity: O(2^D) diamond mixin/base traversal
|
||
|
||
| Field | Value |
|
||
|-------|-------|
|
||
| ID | cpython-0002 |
|
||
| Severity | MEDIUM |
|
||
| Ecosystem | cpython |
|
||
| Package | pydoc |
|
||
| File | `Lib/pydoc.py` |
|
||
| Lines | 232–240 |
|
||
| Complexity | O(2^D) on diamond inheritance hierarchies |
|
||
| Hot path | `pydoc.allmethods(cl)` — collect all methods across inheritance tree |
|
||
|
||
## Defect
|
||
|
||
```python
|
||
# BEFORE (DEFECT) — O(2^D): no visited guard, unconditional recursion
|
||
def allmethods(cl):
|
||
methods = {}
|
||
for key, value in inspect.getmembers(cl, inspect.isroutine):
|
||
methods[key] = 1
|
||
for base in cl.__bases__:
|
||
methods.update(allmethods(base)) # all your base are belong to us
|
||
for key in methods.keys():
|
||
methods[key] = getattr(cl, key)
|
||
return methods
|
||
```
|
||
|
||
With a diamond hierarchy `D → B, C → A` (A shared), `allmethods(D)` recurses into both
|
||
`B` and `C`, each of which recurses into `A`, visiting it twice. At depth D the traversal
|
||
visits `2^D` nodes instead of the O(D) unique classes.
|
||
|
||
## Fix
|
||
|
||
```python
|
||
# AFTER — O(N): guard at entry prevents re-visiting shared ancestors
|
||
def allmethods(cl, _visited=None):
|
||
if _visited is None:
|
||
_visited = set()
|
||
if cl in _visited:
|
||
return {}
|
||
_visited.add(cl)
|
||
methods = {}
|
||
for key, value in inspect.getmembers(cl, inspect.isroutine):
|
||
methods[key] = 1
|
||
for base in cl.__bases__:
|
||
methods.update(allmethods(base, _visited))
|
||
for key in methods.keys():
|
||
methods[key] = getattr(cl, key)
|
||
return methods
|
||
```
|
||
|
||
Note: `inspect.getmro(cl)` already returns the linearized MRO and could replace the
|
||
recursive traversal entirely:
|
||
|
||
```python
|
||
# SIMPLER FIX: use MRO directly (already handles diamond)
|
||
def allmethods(cl):
|
||
methods = {}
|
||
for klass in reversed(inspect.getmro(cl)):
|
||
for key, value in inspect.getmembers(klass, inspect.isroutine):
|
||
methods[key] = 1
|
||
for key in methods.keys():
|
||
methods[key] = getattr(cl, key)
|
||
return methods
|
||
```
|
||
|
||
## Speedup
|
||
|
||
| Diamond depth (D) | Nodes visited (before) | Nodes visited (after) | Speedup |
|
||
|------------------|----------------------|----------------------|---------|
|
||
| 5 | 31 | 6 | 5× |
|
||
| 10 | 1,023 | 11 | 93× |
|
||
| 15 | 32,767 | 16 | 2,048× |
|
||
| 20 | 1,048,575 | 21 | 49,932× |
|
||
|
||
## Notes
|
||
|
||
`allmethods` is defined in `pydoc.py` but is no longer called from the main doc-rendering
|
||
paths (which use `__mro__` directly). However, it remains exported as part of the module's
|
||
public API and is available for external callers. The defect is present and latent.
|