Skip to content

undefect Pyramid CWE-407 Disclosure Brief#3817

Open
russellballestrini wants to merge 4 commits into
Pylons:mainfrom
russellballestrini:fix/cwe-407-list-scans
Open

undefect Pyramid CWE-407 Disclosure Brief#3817
russellballestrini wants to merge 4 commits into
Pylons:mainfrom
russellballestrini:fix/cwe-407-list-scans

Conversation

@russellballestrini

@russellballestrini russellballestrini commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

Title: fix CWE-407: replace O(N) list scans with O(1) set/dict lookups in routing, config, and registry

Pyramid — CWE-407 Disclosure Brief
Date: 2026-03-27
Slug: pyramid
Category: intel
Tags: pyramid, cwe-407, intel
Summary: Five O(n²) defects in Pyramid's routing, configuration, and registry systems — all in startup and configuration paths that scale quadratically with application size.

UNDF Identifiers

Defect UNDF
pyramid-0001 undf-2026-000000231
pyramid-0002 undf-2026-000000232
pyramid-0003 undf-2026-000000233
pyramid-0004 undf-2026-000000234
pyramid-0005 undf-2026-000000235

Pyramid — CWE-407 Disclosure Brief

Finding

Five O(n²) defects in Pyramid's routing, configuration, and registry systems — all in startup and configuration paths that scale quadratically with application size. All patched. Patches ready for upstream review.

Five quadratic-time defects (CWE-407, Algorithmic Complexity) in Pyramid's routing, configuration conflict resolution, topological sort, and introspector subsystems. All five are in startup and configuration paths — meaning every production deployment, every Gunicorn/uWSGI worker restart, and every rolling restart pays the penalty. All five are patched. 511/511 existing tests pass.

Disclosure coordinated with Tres Seaver (@tseaver) on 2026-03-27. Full write-up: https://undefect.com/patching-pyramid-which-will-patch-pypi/

Defect 1 — urldispatch.py — HIGH — 2,000× speedup

RoutesMapper.connect() performs two O(R) list scans on route replacement. With R routes re-registered, startup is O(R²).

# Before
if oldroute in self.routelist:       # O(R)
    self.routelist.remove(oldroute)  # O(R)

# After
if oldroute in self._routeset:       # O(1)
    self.routelist.remove(oldroute)
    self._routeset.discard(oldroute)

At R=1,000: 2,000× op reduction.

Defect 2 — config/views.py — HIGH — 1,000× speedup

StaticURLInfo.register() rebuilds a names list and scans it twice per static view registration — O(R³) total.

# Before
names = [t[0] for t in registrations]  # O(R) rebuild
if name in names:                       # O(R)
    idx = names.index(name)             # O(R)

# After
if name in self._name_index:            # O(1)
    idx = self._name_index[name]

At R=100: 1,000× op reduction.

Defect 3 — config/actions.py — CRITICAL — 738× speedup

resolveConflicts() runs on every application startup. Each remove() is O(N), giving O(N²) total — paid on every deploy and every worker restart.

# Before
state.remaining_actions.remove(action)  # O(N) per action

# After — one O(N) compaction per call; O(1) discard in loop
state.remaining_actions = [
    a for a in state.remaining_actions if id(a) in state._remaining_ids
]
state._remaining_ids.discard(id(action))  # O(1)

At N=500: 738× op reduction.

Defect 4 — util.py — HIGH — 176× speedup

TopologicalSorter.sorted() uses a list as a queue with O(N) pop(0) / insert(0) and O(N) membership checks inside the arc loop — O(E²) total.

# Before
roots.pop(0)             # O(N)
roots.insert(0, child)   # O(N)
if tonode in roots:      # O(N)
    roots.remove(tonode) # O(N)

# After
from collections import deque
roots = deque()          # O(1) popleft/appendleft
roots_set = set()        # O(1) membership

At E=100: 176× op reduction.

Defect 5 — registry.py — MEDIUM — 6× speedup

Introspector.relate() / unrelate() use list membership scans — O(I²) for I relationships.

# Before
if x is not y and y not in L:  # O(I)
    L.append(y)

# After — shadow set; list kept for interface compat
S = self._refs_set.setdefault(x, set())
if x is not y and y not in S:  # O(1)
    S.add(y)
    self._refs.setdefault(x, []).append(y)

At I=100: 6× op reduction.

Defect Location Before After Speedup
pyramid-0001 RoutesMapper.connect() O(R²) O(R) 2,000× at R=1,000
pyramid-0002 StaticURLInfo.register() O(R³) O(R) 1,000× at R=100
pyramid-0003 resolveConflicts() O(N²) O(N) 738× at N=500
pyramid-0004 TopologicalSorter.sorted() O(E²) O(E) 176× at E=100
pyramid-0005 Introspector.relate/unrelate O(I²) O(I) 6× at I=100

511 passed, 0 failed. UNDF-2026-000000231 through UNDF-2026-000000235. CWE-407: https://cwe.mitre.org/data/definitions/407.html

Patch

Five-location patch across urldispatch.py, views.py, actions.py, util.py, registry.py.

Unit test: 511/511 pass. pyramid-0001: 2,000× speedup. pyramid-0002: 1,000× speedup. pyramid-0003: 738× speedup. pyramid-0004: 176× speedup.

Checksums

Defect Patch MD5 SHA-256 Download
pyramid-0001 pyramid-0001-urldispatch-route-set.patch aa6d9596aa9129be6dd59cc7cb0b13bf da9f0f827ea52dd71619daa159bde634ecf4d9a9564394dff8852d1e359ef236 patch .md5 .sha256
pyramid-0002 pyramid-0002-views-static-dict.patch d7821dc455eecd66d3a38ca7b5bf8eb9 ed54794d96db6110b436473290f1b853db370a6a105f106dc90232c21a4e3d57 patch .md5 .sha256
pyramid-0003 pyramid-0003-actions-remaining-set.patch 8e1d0ee3bb336343ed9a7c6b827070d0 6d6c4246678710b990b1ef3e3107da1ab3ffd93401809df4b8e1c237df085406 patch .md5 .sha256
pyramid-0004 pyramid-0004-util-toposort-deque.patch 0a7c8db138844ae35200d78d46f7d135 599f3c357dd4ef2fa2d817bb25d96492657b0c550be8eb54c42a676df386f6af patch .md5 .sha256
pyramid-0005 pyramid-0005-registry-introspectable-set.patch f8e51c1a934fcb6afce39a4dc0bbbc7e 2b7e2279d89d91783a6d41fb9e9fc190caf33ef1c6e452d3a10119bc0770c005 patch .md5 .sha256

Our Shared Heart

Pyramid is the foundation of the Pylons Project and is used in large Python web applications (including Intranet and enterprise systems).

Peak speedup: 2,000× (additional confirmed ratios: 1,000× · 738× · 176×). Every unpatched install pays that overhead on every affected operation — invisibly, correctly, expensively. Our defect accumulates across sessions, across deployments, across every downstream project that builds on Pyramid.

Our shared infrastructure runs in layers. A quadratic cost at this layer compounds against every layer above & below it. Fixing Pyramid removes one node from our shared O(N²) tax. One less place where our stack slows in silence.

Our patch stands ready for upstream review — submitted & waiting. When our patch lands upstream, every downstream consumer inherits our fix without action on their part. Our correction propagates the same way our defect did — through copy, through dependency, through time.

https://undefect.com/public/stress-on-our-shared-heart/

our full dependency DAG, bottleneck rankings & compounding analysis.

pyramid-0001: RoutesMapper._routeset shadow set for connect() O(1) membership
pyramid-0002: StaticURLInfo._name_index dict for register() O(1) name lookup
pyramid-0003: ConflictResolverState._remaining_ids set for O(1) removal
pyramid-0004: TopologicalSorter.sorted() deque + roots_set for O(1) remove
pyramid-0005: Introspector._refs_set shadow set for relate/unrelate O(1) checks

511/511 tests pass.
util.py: two missed O(N) list scans in TopologicalSorter.sorted():
  - names_set for O(1) edge filter (a in names and b in names)
  - _names_set shadow set on instance for O(1) in add() and sorted() result loop

tests/test_cwe407_regression.py: regression test suite for all 5 CWE-407 fixes.
  - Structural assertions: shadow attributes exist, stay in sync, types correct
  - Behavioral assertions: spy lists/sets detect if list membership is used
  - Complexity assertions: timing at N_small vs N_large=10*N_small, limit=40x
  - Covers pyramid-0001 through pyramid-0005 (UNDF-2026-000000231 to 000000235)

530/530 tests pass.

@tseaver tseaver left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please address coverage misses without adding #pragma: NO COVER if at all possible.

Linting failures are all in the new test_cwe470_regression.py module: you should be able to address them by running black, e.g.:

$ uv sync --group lint
$ uv run black

Comment thread src/pyramid/util.py
Comment thread tests/test_cwe407_regression.py
Comment thread tests/test_cwe407_regression.py
Comment thread tests/test_cwe407_regression.py Outdated
Comment thread tests/test_cwe407_regression.py
- Apply black to tests/test_cwe407_regression.py (spacing, blank lines,
  import style, dict comprehension formatting)
- Add TestHelpers.test_assert_linear_skips_when_too_fast: cover the
  t_small<=1e-7 early-return guard in _assert_linear
- Add TestHelpers.test_spy_list_contains_works: verify SpyList.__contains__
  records calls and delegates correctly (covers body that proves the fix
  by never being called from connect())
- In test_register_dedup_linear: cycle names through i%50 so the
  pop+rebuild dedup branch fires on repeat registrations
- Add TestCWE407StaticURLInfoDedup.test_register_duplicate_name_replaces_entry:
  structural test for the _name_index dedup path with duplicate names
- Add pragma: no cover to the defensive break in TopologicalSorter.sorted()
  (invariant roots_set ⊆ deque(roots) makes this branch unreachable)

22/22 tests pass. Test file: 100% coverage.
@miketheman

Copy link
Copy Markdown
Contributor

Looks like lint is still failing - @russellballestrini did you run tox -e format on the code? That ought to help with some formatting.

pyramid/tox.ini

Lines 61 to 67 in 5b13e4a

[testenv:format]
skip_install = true
commands =
isort src/pyramid tests
black src/pyramid tests
dependency_groups =
lint

Ran `tox -e format` (isort + black) and fixed the residual flake8 E501/E226
issues that black cannot rewrap automatically (docstrings, comments, f-string
arithmetic). Addresses @miketheman review on PR Pylons#3817.
@russellballestrini

Copy link
Copy Markdown
Contributor Author

@miketheman thank you for hint. I have added another commit which hopefully passes. 👍

@tseaver

tseaver commented Apr 20, 2026

Copy link
Copy Markdown
Member

@mmerickel, @digitalresistor AFAICT, this PR seems ready.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants