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:
parent
77a5410eee
commit
4d4c256673
8 changed files with 405 additions and 0 deletions
|
|
@ -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 | 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.
|
||||
|
|
@ -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 | 286–293 |
|
||||
| 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.
|
||||
|
|
@ -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 | 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
|
||||
|
||||
```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 179–181):
|
||||
```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.
|
||||
|
|
@ -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 | 868–876 |
|
||||
| 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.
|
||||
15
defects/flask/patch/CLEAN.md
Normal file
15
defects/flask/patch/CLEAN.md
Normal 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.
|
||||
16
defects/phoenix/patch/CLEAN.md
Normal file
16
defects/phoenix/patch/CLEAN.md
Normal 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.
|
||||
17
defects/rails/patch/CLEAN.md
Normal file
17
defects/rails/patch/CLEAN.md
Normal 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.
|
||||
14
defects/ruby/patch/CLEAN.md
Normal file
14
defects/ruby/patch/CLEAN.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue