java-topology/defects/cpython/patch/cpython-0001-pydoc-allmethods-diamond-bases.md
russell@unturf.com 4d4c256673 diamond-scan: cpython/django O(2^D) hierarchy traversal; flask/rails/ruby/phoenix CLEAN
cpython-0001: pydoc.allmethods unconditional __bases__ recursion, no visited guard
cpython-0002: turtle.__methodDict unconditional __bases__ recursion, fires at import
cpython-0003: idlelib.rpc._getmethods unconditional __bases__ recursion, IDLE debug path
django-0006: migrations.state.flatten_bases abstract model diamond traversal (comment in source acknowledges duplicates)
flask/rails/ruby/phoenix: CLEAN
2026-03-29 20:16:12 -04:00

2.6 KiB
Raw Blame History

UNDF: (pending)

cpython-0001: pydoc.allmethods — O(2^D) diamond base traversal

CWE-407 — Algorithmic Complexity: O(2^D) diamond mixin/base traversal

Field Value
ID cpython-0001
Severity MEDIUM
Ecosystem cpython
Package pydoc
File Lib/pydoc.py
Lines 232240
Complexity O(2^D) on diamond inheritance hierarchies
Hot path pydoc.allmethods(cl) — collect all methods across inheritance tree

Defect

# 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

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

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