# UNDF: UNDF-2026-000001269 From: agent-blackops Date: Thu, 26 Mar 2026 00:00:00 +0000 Subject: [PATCH] graph/simple_graph: replace Array path with Set+Array pair in paths_in_cycle BFS CWE-407: Algorithmic complexity via O(|cycle|^3) Array#member? in the BFS loop of paths_in_cycle(). frame[1] is a growing Array used as both the path record and the membership oracle. Each call to frame[1].member?(frame[0]) is O(path_length); paths grow as BFS expands; in the worst case (a fully connected cycle of length N) this produces O(N^3) total comparisons. Fix: replace the bare Array path with a two-field frame [vertex, path_array, path_set] where path_set is a Ruby Set parallel to path_array. Membership testing uses path_set.include?() for O(1) average cost. path_array is retained unchanged so that found paths preserve their ordering and the existing `found.sort` contract is unchanged. Ruby's Set (from 'set') gives O(1) average include? via hash-based storage. No behaviour change; only the membership-test complexity is affected. Defect-Id: PUP-001 Severity: LOW (error path — cycles are uncommon in valid Puppet catalogs) CWE: CWE-407 (Inefficient Algorithmic Complexity) --- lib/puppet/graph/simple_graph.rb | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/puppet/graph/simple_graph.rb b/lib/puppet/graph/simple_graph.rb index xxxxxxx..yyyyyyy 100644 --- a/lib/puppet/graph/simple_graph.rb +++ b/lib/puppet/graph/simple_graph.rb @@ -1,3 +1,4 @@ +require 'set' # CWE-407 fix @@ -199,17 +200,17 @@ class Puppet::Graph::SimpleGraph def paths_in_cycle(cycle, max_paths = 1) # TRANSLATORS "negative or zero" refers to the count of paths raise ArgumentError, _("negative or zero max_paths") if max_paths < 1 # Calculate our filtered outbound vertex lists... adj = {} cycle.each do |vertex| adj[vertex] = adjacent(vertex).select { |s| cycle.member? s } end found = [] - # frame struct is vertex, [path] - stack = [[cycle.first, []]] + # frame struct is vertex, [path_array], path_set # CWE-407 fix + stack = [[cycle.first, [], Set.new]] # CWE-407 fix while frame = stack.shift # rubocop:disable Lint/AssignmentInCondition - if frame[1].member?(frame[0]) then + if frame[2].include?(frame[0]) then # CWE-407 fix: O(1) vs O(path) found << frame[1] + [frame[0]] break if found.length >= max_paths else adj[frame[0]].each do |to| - stack.push [to, frame[1] + [frame[0]]] + new_path = frame[1] + [frame[0]] + new_set = frame[2] | Set[frame[0]] # CWE-407 fix: O(1) insert + stack.push [to, new_path, new_set] # CWE-407 fix end end end found.sort end