1.8 KiB
UNDF: UNDF-2026-000000006
ans-0002: role _load_role_data() collections list O(C) membership tests per role load
Severity: LOW CWE: CWE-407 (Algorithmic Complexity — linear membership test in hot loop) Speedup: ~30x at C=50 collections (verified by unit test) Target: Ansible (ansible/ansible) Files:
lib/ansible/playbook/role/__init__.py:287—c not in self.collectionsgeneratorlib/ansible/playbook/role/__init__.py:293— twonot in self.collectionsguards
Description
_load_role_data() deduplicates collection names using a list:
self.collections.extend((c for c in self._metadata.collections if c not in self.collections))
# ...
if 'ansible.builtin' not in self.collections and 'ansible.legacy' not in self.collections:
self.collections.append(default_append_collection)
Each not in self.collections is O(C) where C = current list length.
Called once per role load; with C=50 collections this is ~155 list scans
instead of 3 hash lookups.
Root Cause
self.collections is a list to preserve insertion order. The list is
queried for membership with O(C) not in tests.
Fix: maintain a parallel _collections_set (Python set) as a shadow
of self.collections. All membership tests become O(1). The list is
retained unchanged so that ordering semantics are preserved;
_collections_set is kept in sync at every mutation site.
Patch
See patch/ans-0002-role-collections-set.patch
Complexity Before
Per not in self.collections check: O(C)
Total per _load_role_data() call: O(C) (3 checks)
Complexity After
Per not in self._collections_set check: O(1) average
Total: O(1)
Reproduction
cd defects/ansible/unit && javac -d . AnsibleRoleTest.java && java -ea unit.AnsibleRoleTest
test3: C=50 candidates, defect=1585, fixed=52, ratio=30.5x