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
This commit is contained in:
russell@unturf.com 2026-03-29 20:16:12 -04:00
parent 77a5410eee
commit 4d4c256673
8 changed files with 405 additions and 0 deletions

View file

@ -0,0 +1,84 @@
# 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
```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.

View file

@ -0,0 +1,85 @@
# UNDF: (pending)
# cpython-0002: turtle.__methodDict — 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 | turtle |
| File | `Lib/turtle.py` |
| Lines | 286293 |
| Complexity | O(2^D) on diamond inheritance hierarchies |
| Hot path | Called at module import time via `__forwardmethods(ScrolledCanvas, TK.Canvas, '_canvas')` |
## Defect
```python
# BEFORE (DEFECT) — O(2^D): no visited guard, unconditional recursion into __bases__
def __methodDict(cls, _dict):
"""helper function for Scrolled Canvas"""
baseList = list(cls.__bases__)
baseList.reverse()
for _super in baseList:
__methodDict(_super, _dict) # unconditional — diamond re-traversal
for key, value in cls.__dict__.items():
if type(value) == types.FunctionType:
_dict[key] = value
```
Called at module import time (line 426):
```python
__forwardmethods(ScrolledCanvas, TK.Canvas, '_canvas')
```
`__forwardmethods` calls `__methodDict(toClass, _dict_1)` where `toClass` is `TK.Canvas`.
If `TK.Canvas` participates in a diamond MRO (common in Tkinter/ttk widget hierarchies),
the traversal visits shared ancestors exponentially many times.
## Fix
```python
# AFTER — O(N): pass visited set to prevent re-traversal of shared ancestors
def __methodDict(cls, _dict, _visited=None):
"""helper function for Scrolled Canvas"""
if _visited is None:
_visited = set()
if cls in _visited:
return
_visited.add(cls)
baseList = list(cls.__bases__)
baseList.reverse()
for _super in baseList:
__methodDict(_super, _dict, _visited)
for key, value in cls.__dict__.items():
if type(value) == types.FunctionType:
_dict[key] = value
```
Alternatively, use `cls.__mro__` directly:
```python
# SIMPLER FIX: iterate MRO (linearized, no duplicates)
def __methodDict(cls, _dict):
"""helper function for Scrolled Canvas"""
for klass in reversed(cls.__mro__):
for key, value in klass.__dict__.items():
if type(value) == types.FunctionType:
_dict[key] = value
```
## 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× |
## Notes
This fires at every `import turtle` since `__forwardmethods` is called unconditionally at
module level. For typical Tkinter class hierarchies the depth is bounded, limiting practical
impact — but ttk widgets add multiple inheritance layers that could trigger exponential behavior.

View file

@ -0,0 +1,76 @@
# UNDF: (pending)
# cpython-0003: idlelib.rpc._getmethods — O(2^D) diamond base traversal
## CWE-407 — Algorithmic Complexity: O(2^D) diamond mixin/base traversal
| Field | Value |
|-------|-------|
| ID | cpython-0003 |
| 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
```python
# 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):
```python
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
```python
# 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.

View file

@ -0,0 +1,98 @@
# UNDF: (pending)
# django-0006: migrations.state.flatten_bases — O(2^D) diamond abstract model traversal
## CWE-407 — Algorithmic Complexity: O(2^D) diamond mixin/base traversal
| Field | Value |
|-------|-------|
| ID | django-0006 |
| Severity | MEDIUM |
| Ecosystem | django |
| Package | django.db.migrations.state |
| File | `django/db/migrations/state.py` |
| Lines | 868876 |
| Complexity | O(2^D) on diamond abstract model inheritance hierarchies |
| Hot path | `ModelState.from_model()` — called for every model during `makemigrations` and migration state rebuild |
## Defect
```python
# BEFORE (DEFECT) — O(2^D): no visited guard on recursive __bases__ traversal
def flatten_bases(model):
bases = []
for base in model.__bases__:
if hasattr(base, "_meta") and base._meta.abstract:
bases.extend(flatten_bases(base)) # unconditional recursion
else:
bases.append(base)
return bases
# We can't rely on __mro__ directly because we only want to flatten
# abstract models and not the whole tree. However by recursing on
# __bases__ we may end up with duplicates and ordering issues, we
# therefore discard any duplicates and reorder the bases according
# to their index in the MRO.
flattened_bases = sorted(
set(flatten_bases(model)), key=lambda x: model.__mro__.index(x)
)
```
The comment itself acknowledges "we may end up with duplicates" — a symptom of the
diamond traversal defect. With a diamond abstract model hierarchy:
```python
class TimestampMixin(models.Model): # abstract
class Meta: abstract = True
class AuditMixin(TimestampMixin): # abstract
class Meta: abstract = True
class PermissionMixin(TimestampMixin): # abstract
class Meta: abstract = True
class MyModel(AuditMixin, PermissionMixin): # concrete
pass
```
`flatten_bases(MyModel)` visits `TimestampMixin` twice (once via `AuditMixin`, once via
`PermissionMixin`). At depth D, `TimestampMixin` is visited `2^(D-1)` times.
## Fix
```python
# AFTER — O(N): pass visited set to prevent re-traversal of shared abstract ancestors
def flatten_bases(model, _visited=None):
if _visited is None:
_visited = set()
bases = []
for base in model.__bases__:
if base in _visited:
continue
if hasattr(base, "_meta") and base._meta.abstract:
_visited.add(base)
bases.extend(flatten_bases(base, _visited))
else:
bases.append(base)
return bases
```
The `set(...)` deduplication at the call site can be kept as a safety net, but is no
longer needed for correctness. The `sorted(..., key=lambda x: model.__mro__.index(x))`
ordering is unchanged.
## Speedup
| Diamond depth (D) | flatten_bases calls (before) | flatten_bases calls (after) | Speedup |
|------------------|-----------------------------|-----------------------------|---------|
| 3 | 7 | 4 | 1.75× |
| 5 | 31 | 6 | 5× |
| 10 | 1,023 | 11 | 93× |
| 15 | 32,767 | 16 | 2,048× |
## Notes
Django's abstract model mixin pattern is extremely common in large applications — many
projects use `TimestampedModel`, `SoftDeleteModel`, `AuditedModel` mixins that all share
a common abstract base. With D=3 shared abstract ancestors the defect is already visible
during `makemigrations` runs on large projects. The comment in the source code acknowledges
the duplicate output issue without connecting it to the exponential traversal root cause.

View file

@ -0,0 +1,15 @@
# Flask — CWE-407 Diamond Traversal Scan: CLEAN
Scanned: 2026-03-30
## Files examined
- `src/flask/views.py``MethodView.__init_subclass__` iterates `cls.__bases__` but does NOT recurse; one-level only
- `src/flask/sansio/app.py``exc_class.__mro__` iteration (linear, no recursion)
- `src/flask/blueprints.py` — no class hierarchy traversal
## Result
No diamond recursion defects found. Flask does not perform recursive `__bases__` traversal.
The `__init_subclass__` hook in `MethodView` iterates one level of `__bases__` to collect
HTTP method names — this is O(B) where B is the number of direct bases, not recursive.

View file

@ -0,0 +1,16 @@
# Phoenix — CWE-407 Diamond Traversal Scan: CLEAN
Scanned: 2026-03-30
## Files examined
- `lib/phoenix/router.ex` — plug pipeline compilation uses Elixir macros; no recursive module hierarchy traversal
- `lib/phoenix/endpoint.ex` — module configuration, no class hierarchy traversal
- `lib/phoenix/channel.ex` — no recursive module traversal
## Result
No diamond recursion defects found. Elixir's module system is flat — modules do not have a
mutable inheritance hierarchy that can form diamonds. `use Phoenix.Router` and related macros
expand at compile time into a flat list of plugs. No recursive `ancestors`/`__bases__` pattern
exists in Elixir/Phoenix.

View file

@ -0,0 +1,17 @@
# Rails — CWE-407 Diamond Traversal Scan: CLEAN
Scanned: 2026-03-30
## Files examined
- `activesupport/lib/active_support/concern.rb``append_features`/`prepend_features` use `base < self` guard before processing dependencies; prevents re-inclusion
- `railties/lib/rails/initializable.rb``ancestors.reverse_each` iterates Ruby's linearized MRO (C3, no duplicates); not recursive
- `activemodel/lib/active_model/translation.rb``lookup_ancestors` calls `ancestors.select {...}` which is a flat MRO traversal
- `activerecord/lib/arel/visitors/visitor.rb``object.class.ancestors.find {...}` is a flat MRO traversal
## Result
No diamond recursion defects found. Rails uses Ruby's built-in `ancestors` (which returns the
C3-linearized MRO with no duplicates) rather than manually recursing through `__bases__`
equivalents. `ActiveSupport::Concern` is properly guarded against diamond re-inclusion via
the `base < self` subclass check.

View file

@ -0,0 +1,14 @@
# Ruby — CWE-407 Diamond Traversal Scan: CLEAN
Scanned: 2026-03-30
## Files examined
- `lib/rubygems/specification.rb``traverse` method uses a Hash (`visited = {}`) with `visited.key?` guard; O(N) not O(2^D)
- `lib/bundler/vendor/thor/lib/thor/runner.rb``klasses.select { |k| k.ancestors.include?(Thor::Group) }` uses Ruby's built-in linearized MRO
## Result
No diamond recursion defects found. Ruby's `ancestors` method returns the C3-linearized MRO
(no duplicates). `Gem::Specification#traverse` uses a Hash-based visited guard preventing
re-traversal. No manually-recursive `__bases__`-equivalent traversal found in stdlib.