java-topology/defects/cpython/patch/cpython-0001-mock-reset-visited-hashset.md
russell@unturf.com 7491349edc game-engines: allegro5/bullet3/box2d/dry CWE-407 scan
bullet3-0001: btGhostObject::addOverlappingObjectInternal O(N²) linear dedup
  per broadphase step — even carries "too slow" self-admission comment (HIGH)
bullet3-0002: btSoftRigidCollisionAlgorithm::processCollision O(C×D) per
  frame on m_collisionDisabledObjects plain array (MEDIUM)
allegro5: CLEAN (vector_contains only on non-hot setup paths)
box2d: CLEAN (v3 rewrite uses b2HashSet throughout)
dry: CLEAN (HashSet/HashMap on all hot dedup paths)
2026-03-29 19:50:49 -04:00

81 lines
2.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# UNDF: UNDF-2026-000000039
# UNDF: (pending)
# cpython-0001: unittest.mock.Mock.reset_mock — O(N²) visited list in diamond mock tree traversal
## CWE-407 — Algorithmic Complexity
| Field | Value |
|-------|-------|
| ID | cpython-0001 |
| Severity | MEDIUM |
| Ecosystem | cpython |
| Package | unittest.mock |
| File | `Lib/unittest/mock.py` |
| Lines | 638667 |
| Complexity | O(N²) list membership, O(N) with set |
| Hot path | `mock.reset_mock()` on large/deeply-nested MagicMock trees |
## Defect
`reset_mock` uses a `list` for cycle detection when traversing the mock object
tree. On each recursive call, `id(self) in visited` performs an O(N) linear
scan of all previously visited mock IDs. For a mock tree with N nodes (children
plus return_value chains), the total membership-check cost is O(1+2+…+N) =
O(N²).
MagicMock auto-creates child mocks on attribute access, so test suites that
build large spec-based or deeply-patched mocks can trigger this path. A mock
with 1,000 children (e.g. a module-level patch with many methods) causes ~500k
comparisons instead of ~1k.
```python
# BEFORE — O(N²): list membership O(N) per recursive call
def reset_mock(self, visited=None, *, return_value=False, side_effect=False):
if visited is None:
visited = [] # list, not set
if id(self) in visited: # O(N) linear scan
return
visited.append(id(self)) # O(1) append but scan above is O(N)
for child in self._mock_children.values():
if isinstance(child, _SpecState) or child is _deleted:
continue
child.reset_mock(visited, ...) # recurse, visited grows
ret = self._mock_return_value
if _is_instance_mock(ret) and ret is not self:
ret.reset_mock(visited)
```
## Fix
Replace the `list` with a `set`. Integer `id` values hash in O(1) and set
membership is O(1) average.
```python
# AFTER — O(N): set membership O(1) per call
def reset_mock(self, visited=None, *, return_value=False, side_effect=False):
if visited is None:
visited = set() # set, not list
if id(self) in visited: # O(1) hash lookup
return
visited.add(id(self)) # O(1) insert
# ... rest unchanged
```
## Speedup
| Mock nodes (N) | Before (ops) | After (ops) | Speedup |
|----------------|--------------|-------------|---------|
| 100 | 5,050 | 100 | 50× |
| 500 | 125,250 | 500 | 250× |
| 1,000 | 500,500 | 1,000 | 500× |
| 2,000 | 2,001,000 | 2,000 | 1,000× |
## Notes
- The same pattern appears in `_mock_check_sig` / other recursive helpers; only
`reset_mock` is flagged here as the primary confirmed path.
- `id` values are unique integers per live object — set storage is safe.
- No semantic change: cycle detection behaviour is identical.
- Fix applies to CPython main as of depth-1 clone (HEAD ~May 2025).