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)
2.5 KiB
2.5 KiB
UNDF: UNDF-2026-000000675
UNDF: (pending)
cpython-0004: idlelib.rpc._getmethods — O(2^D) diamond base traversal
CWE-407 — Algorithmic Complexity: O(2^D) diamond mixin/base traversal
| Field | Value |
|---|---|
| ID | cpython-0004 |
| Severity | MEDIUM |
| Ecosystem | cpython |
| Package | idlelib.rpc |
| File | Lib/idlelib/rpc.py |
| Lines | 581–590 |
| Complexity | O(2^D) on diamond inheritance hierarchies |
| Hot path | Triggered when IDLE debugger calls __methods__ on a remote object (every debug session inspect) |
Defect
# BEFORE (DEFECT) — O(2^D): no visited guard, unconditional recursion into __bases__
def _getmethods(obj, methods):
# Helper to get a list of methods from an object
# Adds names to dictionary argument 'methods'
for name in dir(obj):
attr = getattr(obj, name)
if callable(attr):
methods[name] = 1
if isinstance(obj, type):
for super in obj.__bases__:
_getmethods(super, methods) # unconditional — diamond re-traversal
Called via IDLE RPC when someone requests __methods__ (line 179–181):
if methodname == "__methods__":
methods = {}
_getmethods(obj, methods)
With a diamond hierarchy, each shared ancestor is visited 2^D times instead of once.
dir(obj) is called on each visit, making the actual cost O(2^D × M) where M is the
number of methods per class.
Fix
# AFTER — O(N): pass visited set to prevent re-traversal of shared ancestors
def _getmethods(obj, methods, _visited=None):
if _visited is None:
_visited = set()
if obj in _visited:
return
_visited.add(obj)
for name in dir(obj):
attr = getattr(obj, name)
if callable(attr):
methods[name] = 1
if isinstance(obj, type):
for super in obj.__bases__:
_getmethods(super, methods, _visited)
Speedup
| Diamond depth (D) | dir() calls (before) |
dir() calls (after) |
Speedup |
|---|---|---|---|
| 5 | 31 | 6 | 5× |
| 10 | 1,023 | 11 | 93× |
| 15 | 32,767 | 16 | 2,048× |
Notes
dir() is an expensive call (it consults __dict__, __dir__, and all descriptor slots).
In a deep diamond hierarchy under the IDLE debugger, inspecting a complex object could
cause the IDLE UI to stall for seconds. The methods dict deduplication only prevents
incorrect output — it does not prevent the exponential traversal cost.