java-topology/defects/cpython/patch/cpython-0004-idlelib-rpc-getmethods-diamond-bases.md
russell@unturf.com 130e4fb159 undf: assign 671-680 to diamond-scan defects; 680 total assigned
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)
2026-03-29 20:27:09 -04:00

2.5 KiB
Raw Blame History

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 581590
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 179181):

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.