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