java-topology/defects/ansible/patch/ansible-0001-role-get-vars-seen-list.md

2.2 KiB
Raw Blame History

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 539546

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