terraform-0001/ansible-0002/ansible-0003: CWE-407 deeper scan — 3 defects, 6/6 PASS

terraform-0001: Tarjan SCC inStack linear scan O(E*V) — HIGH, 994x at V=2000
ansible-0002: Host.add_group() list membership O(A*G) — MEDIUM, 499x at G=1000
ansible-0003: Handler.notify_host() list membership O(H^2) — MEDIUM, 499x at H=1000
This commit is contained in:
russell@unturf.com 2026-03-30 13:29:25 -04:00
parent 3d2957276d
commit e0aba0e21e
5 changed files with 469 additions and 0 deletions

View file

@ -0,0 +1,52 @@
# UNDF: (leave blank)
# CWE-407: Host.add_group() and populate_ancestors() use `group not in self.groups`
# linear scan on list inside loops — O(A*G) per add_group call, O(H*G*A) total
# Fix: maintain _groups_set: set alongside self.groups for O(1) membership
# Severity: MEDIUM — inventory with 100+ groups per host triggers quadratic behavior
# Measured: 250x op-count overhead at G=500
--- a/lib/ansible/inventory/host.py
+++ b/lib/ansible/inventory/host.py
@@ -62,6 +62,7 @@
self.vars: dict[str, t.Any] = {}
self.groups: list[Group] = []
+ self._groups_set: set[Group] = set()
self._uuid: str | None = None
@@ -80,8 +81,10 @@
def populate_ancestors(self, additions: c.Iterable[Group] | None = None) -> None:
if additions is None:
for group in self.groups:
self.add_group(group)
else:
for group in additions:
- if group not in self.groups:
+ if group not in self._groups_set:
self.groups.append(group)
+ self._groups_set.add(group)
@@ -90,13 +93,15 @@
def add_group(self, group: Group) -> bool:
added = False
# populate ancestors first
for oldg in group.get_ancestors():
- if oldg not in self.groups:
+ if oldg not in self._groups_set:
self.groups.append(oldg)
+ self._groups_set.add(oldg)
# actually add group
- if group not in self.groups:
+ if group not in self._groups_set:
self.groups.append(group)
+ self._groups_set.add(group)
added = True
return added
@@ -103,7 +108,8 @@
def remove_group(self, group: Group) -> bool:
removed = False
- if group in self.groups:
+ if group in self._groups_set:
self.groups.remove(group)
+ self._groups_set.discard(group)
removed = True

View file

@ -0,0 +1,39 @@
# UNDF: (leave blank)
# CWE-407: Handler.is_host_notified() uses `host in self.notified_hosts` linear scan
# on list — O(H) per notification, O(H^2) total across all hosts
# Fix: maintain _notified_hosts_set: set alongside self.notified_hosts for O(1)
# Severity: MEDIUM — playbooks with 100+ hosts triggering same handler
# Measured: 250x op-count overhead at H=500
--- a/lib/ansible/playbook/handler.py
+++ b/lib/ansible/playbook/handler.py
@@ -29,6 +29,7 @@
def __init__(self, block=None, role=None, task_include=None):
self.notified_hosts = []
+ self._notified_hosts_set = set()
self.cached_name = False
@@ -55,6 +56,7 @@
def notify_host(self, host):
if not self.is_host_notified(host):
self.notified_hosts.append(host)
+ self._notified_hosts_set.add(host)
return True
return False
@@ -61,6 +63,7 @@
def remove_host(self, host):
try:
self.notified_hosts.remove(host)
+ self._notified_hosts_set.discard(host)
except ValueError:
raise AnsibleAssertionError(
@@ -69,5 +72,6 @@
def clear_hosts(self):
self.notified_hosts = []
+ self._notified_hosts_set = set()
def is_host_notified(self, host):
- return host in self.notified_hosts
+ return host in self._notified_hosts_set

View file

@ -0,0 +1,132 @@
import java.util.*;
/**
* CWE-407 unit tests for ansible-0002 and ansible-0003.
*
* ansible-0002: Host.add_group() uses `group not in self.groups` list scan O(G)
* inside ancestor loop O(A*G) per call, O(H*G*A) total.
*
* ansible-0003: Handler.is_host_notified() uses `host in self.notified_hosts`
* list scan O(H) per notify, O(H^2) total.
*
* Fix: set alongside list for O(1) membership.
*/
public class AnsibleTest {
static long ops;
// ===== ansible-0002: Host.add_group linear scan =====
/** Simulates adding G groups to a host, each with some ancestors */
static long hostAddGroupDefective(int G) {
ops = 0;
List<String> hostGroups = new ArrayList<>(); // self.groups as list
for (int g = 0; g < G; g++) {
// Simulate: group not in self.groups (linear scan)
boolean found = false;
for (String existing : hostGroups) {
ops++;
if (existing.equals("group-" + g)) { found = true; break; }
}
if (!found) {
hostGroups.add("group-" + g);
}
}
return ops;
}
static long hostAddGroupFixed(int G) {
ops = 0;
List<String> hostGroups = new ArrayList<>();
Set<String> hostGroupsSet = new HashSet<>();
for (int g = 0; g < G; g++) {
ops++; // HashSet.contains = O(1), count as 1 op
if (!hostGroupsSet.contains("group-" + g)) {
hostGroups.add("group-" + g);
hostGroupsSet.add("group-" + g);
}
}
return ops;
}
// ===== ansible-0003: Handler.notify_host linear scan =====
/** Simulates notifying H hosts on a handler */
static long handlerNotifyDefective(int H) {
ops = 0;
List<String> notifiedHosts = new ArrayList<>(); // self.notified_hosts as list
for (int h = 0; h < H; h++) {
String host = "host-" + h;
// is_host_notified: host in self.notified_hosts (linear scan)
boolean found = false;
for (String existing : notifiedHosts) {
ops++;
if (existing.equals(host)) { found = true; break; }
}
if (!found) {
notifiedHosts.add(host);
}
}
return ops;
}
static long handlerNotifyFixed(int H) {
ops = 0;
List<String> notifiedHosts = new ArrayList<>();
Set<String> notifiedSet = new HashSet<>();
for (int h = 0; h < H; h++) {
String host = "host-" + h;
ops++; // HashSet.contains = O(1)
if (!notifiedSet.contains(host)) {
notifiedHosts.add(host);
notifiedSet.add(host);
}
}
return ops;
}
public static void main(String[] args) {
int[] sizes = {200, 500, 1000};
boolean allPass = true;
// --- ansible-0002 ---
System.out.println("ansible-0002: Host.add_group() list membership scan");
System.out.println("===================================================");
System.out.printf("%-8s %14s %14s %10s %s%n",
"G", "Defect(ops)", "Fixed(ops)", "Ratio", "Status");
for (int G : sizes) {
long dOps = hostAddGroupDefective(G);
long fOps = hostAddGroupFixed(G);
double ratio = (double) dOps / Math.max(fOps, 1);
String status = (ratio >= 5.0) ? "PASS" : "FAIL";
if (!status.equals("PASS")) allPass = false;
System.out.printf("%-8d %14d %14d %10.1fx %s%n", G, dOps, fOps, ratio, status);
}
System.out.println();
// --- ansible-0003 ---
System.out.println("ansible-0003: Handler.notify_host() list membership scan");
System.out.println("=======================================================");
System.out.printf("%-8s %14s %14s %10s %s%n",
"H", "Defect(ops)", "Fixed(ops)", "Ratio", "Status");
for (int H : sizes) {
long dOps = handlerNotifyDefective(H);
long fOps = handlerNotifyFixed(H);
double ratio = (double) dOps / Math.max(fOps, 1);
String status = (ratio >= 5.0) ? "PASS" : "FAIL";
if (!status.equals("PASS")) allPass = false;
System.out.printf("%-8d %14d %14d %10.1fx %s%n", H, dOps, fOps, ratio, status);
}
System.out.println();
System.out.println(allPass ? "ALL PASS" : "SOME FAIL");
System.exit(allPass ? 0 : 1);
}
}

View file

@ -0,0 +1,57 @@
# UNDF: UNDF-2026-000000307
# UNDF: (leave blank)
# CWE-407: Tarjan SCC inStack linear scan O(V) inside edge loop = O(E*V) total
# Fix: add inStack map[Vertex]bool for O(1) membership test
# Severity: HIGH — Terraform DAG can have thousands of vertices in large configs
# Measured: 250x overhead at V=500, E=2000 (edge-per-vertex ratio ~4)
--- a/internal/dag/tarjan.go
+++ b/internal/dag/tarjan.go
@@ -61,6 +61,7 @@
type sccAcct struct {
NextIndex int
VertexIndex map[Vertex]int
+ InStack map[Vertex]bool
Stack []Vertex
SCC [][]Vertex
}
@@ -72,11 +73,13 @@
s.VertexIndex[v] = idx
s.NextIndex++
s.push(v)
return idx
}
// push adds a vertex to the stack
func (s *sccAcct) push(n Vertex) {
s.Stack = append(s.Stack, n)
+ s.InStack[n] = true
}
// pop removes a vertex from the stack
@@ -86,12 +89,13 @@
return nil
}
vertex := s.Stack[n-1]
s.Stack = s.Stack[:n-1]
+ delete(s.InStack, vertex)
return vertex
}
// inStack checks if a vertex is in the stack
func (s *sccAcct) inStack(needle Vertex) bool {
- for _, n := range s.Stack {
- if n == needle {
- return true
- }
- }
- return false
+ return s.InStack[needle]
}
--- a/internal/dag/dag.go (constructor)
+++ b/internal/dag/dag.go (constructor)
@@ -11,6 +11,7 @@
acct := sccAcct{
NextIndex: 1,
VertexIndex: make(map[Vertex]int, len(vs)),
+ InStack: make(map[Vertex]bool, len(vs)),
}

View file

@ -0,0 +1,189 @@
import java.util.*;
/**
* CWE-407 unit test for terraform-0001: Tarjan SCC inStack linear scan.
*
* Defect: sccAcct.inStack() scans the Stack slice linearly O(V) per call.
* Called from stronglyConnected() for every edge to an already-visited vertex.
* Total complexity: O(E*V) instead of O(V+E).
*
* Fix: maintain a HashSet (map[Vertex]bool in Go) for O(1) membership.
*/
public class TerraformTest {
static long defectOps;
static long fixedOps;
// --- Graph representation ---
static class Graph {
int V;
List<List<Integer>> adj;
Graph(int V) {
this.V = V;
this.adj = new ArrayList<>(V);
for (int i = 0; i < V; i++) adj.add(new ArrayList<>());
}
void addEdge(int u, int v) {
adj.get(u).add(v);
}
}
// --- DEFECTIVE: Tarjan with linear inStack scan ---
static List<List<Integer>> tarjanDefective(Graph g) {
int[] index = new int[g.V];
int[] lowlink = new int[g.V];
List<Integer> stack = new ArrayList<>();
List<List<Integer>> sccs = new ArrayList<>();
int[] nextIndex = {1};
defectOps = 0;
for (int v = 0; v < g.V; v++) {
if (index[v] == 0) {
strongConnectDefective(g, v, index, lowlink, stack, sccs, nextIndex);
}
}
return sccs;
}
static void strongConnectDefective(Graph g, int v, int[] index, int[] lowlink,
List<Integer> stack, List<List<Integer>> sccs,
int[] nextIndex) {
index[v] = nextIndex[0];
lowlink[v] = nextIndex[0];
nextIndex[0]++;
stack.add(v);
for (int w : g.adj.get(v)) {
if (index[w] == 0) {
strongConnectDefective(g, w, index, lowlink, stack, sccs, nextIndex);
lowlink[v] = Math.min(lowlink[v], lowlink[w]);
} else if (inStackLinear(stack, w)) { // DEFECT: O(|stack|) per edge
lowlink[v] = Math.min(lowlink[v], index[w]);
}
}
if (lowlink[v] == index[v]) {
List<Integer> scc = new ArrayList<>();
int w;
do {
w = stack.remove(stack.size() - 1);
scc.add(w);
} while (w != v);
sccs.add(scc);
}
}
/** DEFECT: linear scan of stack -- O(|stack|) per call */
static boolean inStackLinear(List<Integer> stack, int needle) {
for (int n : stack) {
defectOps++; // count comparisons
if (n == needle) return true;
}
return false;
}
// --- FIXED: Tarjan with HashSet for inStack ---
static List<List<Integer>> tarjanFixed(Graph g) {
int[] index = new int[g.V];
int[] lowlink = new int[g.V];
Set<Integer> inStack = new HashSet<>();
List<Integer> stack = new ArrayList<>();
List<List<Integer>> sccs = new ArrayList<>();
int[] nextIndex = {1};
fixedOps = 0;
for (int v = 0; v < g.V; v++) {
if (index[v] == 0) {
strongConnectFixed(g, v, index, lowlink, stack, inStack, sccs, nextIndex);
}
}
return sccs;
}
static void strongConnectFixed(Graph g, int v, int[] index, int[] lowlink,
List<Integer> stack, Set<Integer> inStack,
List<List<Integer>> sccs, int[] nextIndex) {
index[v] = nextIndex[0];
lowlink[v] = nextIndex[0];
nextIndex[0]++;
stack.add(v);
inStack.add(v);
for (int w : g.adj.get(v)) {
if (index[w] == 0) {
strongConnectFixed(g, w, index, lowlink, stack, inStack, sccs, nextIndex);
lowlink[v] = Math.min(lowlink[v], lowlink[w]);
} else {
fixedOps++; // count HashSet.contains call (O(1))
if (inStack.contains(w)) {
lowlink[v] = Math.min(lowlink[v], index[w]);
}
}
}
if (lowlink[v] == index[v]) {
List<Integer> scc = new ArrayList<>();
int w;
do {
w = stack.remove(stack.size() - 1);
inStack.remove(w);
scc.add(w);
} while (w != v);
sccs.add(scc);
}
}
// --- Build worst-case graph: single large SCC (cycle of V vertices) ---
static Graph buildLargeSCC(int V) {
Graph g = new Graph(V);
for (int i = 0; i < V - 1; i++) {
g.addEdge(i, i + 1);
}
g.addEdge(V - 1, 0); // close the cycle
// Add cross-edges to increase edge count and inStack pressure
Random rng = new Random(42);
for (int i = 0; i < V * 3; i++) {
int src = rng.nextInt(V);
int target = rng.nextInt(V);
if (target != src) g.addEdge(src, target);
}
return g;
}
public static void main(String[] args) {
int[] sizes = {500, 1000, 2000};
System.out.println("terraform-0001: Tarjan SCC inStack linear scan");
System.out.println("==============================================");
System.out.printf("%-8s %14s %14s %10s %s%n",
"V", "Defect(ops)", "Fixed(ops)", "Ratio", "Status");
boolean allPass = true;
for (int V : sizes) {
Graph g = buildLargeSCC(V);
// Correctness: both must find same number of SCCs
List<List<Integer>> sccDef = tarjanDefective(g);
long dOps = defectOps;
List<List<Integer>> sccFix = tarjanFixed(g);
long fOps = fixedOps;
boolean correct = (sccDef.size() == sccFix.size());
double ratio = (double) dOps / Math.max(fOps, 1);
// At V=500 with ~2000 edges, defective should do ~250x more ops
String status = (ratio >= 5.0 && correct) ? "PASS" : "FAIL";
if (!status.equals("PASS")) allPass = false;
System.out.printf("%-8d %14d %14d %10.1fx %s%n",
V, dOps, fOps, ratio, status);
}
System.out.println();
System.out.println(allPass ? "ALL PASS" : "SOME FAIL");
System.exit(allPass ? 0 : 1);
}
}