2.2 KiB
2.2 KiB
UNDF: UNDF-2026-000000005
ansible-0001: Role.get_vars() seen-list O(D²) deduplication
Classification
- Severity: MEDIUM
- CWE: CWE-407 (Algorithmic Complexity — Inefficient Algorithmic Complexity)
- Component:
lib/ansible/playbook/role/__init__.py
Location
lib/ansible/playbook/role/__init__.py, method get_vars(), lines 539–546
seen = []
for dep in self.get_all_dependencies():
# Avoid rerunning dupe deps since they can have vars from previous invocations
if dep not in seen:
all_vars = combine_vars(all_vars, dep.get_vars(include_params=False, only_exports=True))
seen.append(dep)
Pattern
seen is initialized as a Python list. The membership test dep not in seen is O(D) for each of D dependencies → total O(D²). In a large Ansible playbook with deeply nested roles (e.g. enterprise roles with D=100+ transitive dependencies), this produces D(D-1)/2 comparisons.
Speedup
At D=200 dependencies: 19,900 comparisons → 200 comparisons (99.5x reduction)
Patch
--- a/lib/ansible/playbook/role/__init__.py
+++ b/lib/ansible/playbook/role/__init__.py
@@ -536,10 +536,10 @@ class Role(Base, Become, Conditional, Taggable, CollectionSearch):
# get exported variables from meta/dependencies
- seen = []
+ seen = set()
for dep in self.get_all_dependencies():
# Avoid rerunning dupe deps since they can have vars from previous invocations and they accumulate in deps
# TODO: re-examine dep loading to see if we are somehow improperly adding the same dep too many times
if dep not in seen:
# only take 'exportable' vars from deps
all_vars = combine_vars(all_vars, dep.get_vars(include_params=False, only_exports=True))
- seen.append(dep)
+ seen.add(dep)
Note: Role objects are used as set members; Python uses identity (id()) by default for unhashed objects, which is correct here — same object in memory = same dep. If Role doesn't define __hash__, Python uses the default identity hash.
Complexity
- Before: O(D²) — D = number of transitive role dependencies
- After: O(D) — set membership is O(1) amortized