java-topology/defects/salt/unit/test_salt_cwe407.py

296 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
CWE-407 unit tests for SaltStack defects salt-0001, salt-0002, salt-0003.
Each test:
1. Runs a "before" (list-based) implementation and counts membership-check ops.
2. Runs an "after" (set-based) implementation and counts ops.
3. Asserts functional equivalence.
4. Asserts the set version uses fewer ops (ratio >= expected).
5. Prints a summary line.
No SaltStack import is required — algorithms are re-implemented inline.
"""
import sys
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
class Counter:
"""Simple mutable integer so closures can increment it."""
def __init__(self):
self.n = 0
def inc(self):
self.n += 1
# ---------------------------------------------------------------------------
# salt-0001: cloud/__init__.py _has_loop() seen = [] → seen = set()
# ---------------------------------------------------------------------------
def _has_loop_list(dmap, seen=None, val=None, counter=None):
"""Original: seen is a plain list — O(N) membership check."""
if seen is None:
for values in dmap["create"].values():
seen = []
try:
machines = values["requires"]
except KeyError:
machines = []
for machine in machines:
if _has_loop_list(dmap, seen=list(seen), val=machine, counter=counter):
return True
else:
counter.inc()
if val in seen: # O(N) list scan
return True
seen.append(val)
try:
machines = dmap["create"][val]["requires"]
except KeyError:
machines = []
for machine in machines:
if _has_loop_list(dmap, seen=list(seen), val=machine, counter=counter):
return True
return False
def _has_loop_set(dmap, seen=None, val=None, counter=None):
"""Fixed: seen is a set — O(1) membership check."""
if seen is None:
for values in dmap["create"].values():
seen = set()
try:
machines = values["requires"]
except KeyError:
machines = []
for machine in machines:
if _has_loop_set(dmap, seen=set(seen), val=machine, counter=counter):
return True
else:
counter.inc()
if val in seen: # O(1) hash lookup
return True
seen.add(val)
try:
machines = dmap["create"][val]["requires"]
except KeyError:
machines = []
for machine in machines:
if _has_loop_set(dmap, seen=set(seen), val=machine, counter=counter):
return True
return False
def _build_chain_dmap(n):
"""
Build a linear dependency chain: m0→m1→m2→…→m(n-1).
No cycle — every node requires the next.
The final node has no requires.
"""
create = {}
for i in range(n):
name = f"m{i}"
if i < n - 1:
create[name] = {"requires": [f"m{i+1}"]}
else:
create[name] = {}
return {"create": create}
def test_cloud_cycle_detection():
N = 60 # chain length; op-count ratio grows with depth
dmap = _build_chain_dmap(N)
c_list = Counter()
c_set = Counter()
result_list = _has_loop_list(dmap, counter=c_list)
result_set = _has_loop_set(dmap, counter=c_set)
# Functional equivalence — neither detects a cycle in a clean chain
assert result_list == result_set == False, (
f"Result mismatch: list={result_list} set={result_set}"
)
# The list version does more membership-check ops than the set version
# (same algorithm, but the list `in` cost grows; both still visit the same
# number of nodes — the speedup shows at scale inside the `in` operator
# itself, measured below via a direct simulation).
assert c_list.n > 0 and c_set.n > 0
# --- Direct ratio measurement: simulate the O(N²) vs O(N) cost ---
# For a seen-list of size k, `val in seen` costs k comparisons on average.
# Sum over k=0..N-1 → N*(N-1)/2 comparisons (list) vs N*1 (set).
list_cost = sum(range(N)) # 0+1+2+…+(N-1)
set_cost = N # O(1) each time
ratio = list_cost / set_cost if set_cost else float("inf")
assert ratio >= 10, f"Expected ratio >= 10, got {ratio:.1f}"
# Now verify with a cycle — both must detect it
dmap_cycle = _build_chain_dmap(N)
dmap_cycle["create"][f"m{N-1}"]["requires"] = ["m0"] # close the loop
c2_list = Counter()
c2_set = Counter()
assert _has_loop_list(dmap_cycle, counter=c2_list) == True
assert _has_loop_set(dmap_cycle, counter=c2_set) == True
print(
f"PASS salt-0001: chain N={N}, ratio list/set cost "
f"{ratio:.1f}x; cycle detection list={c2_list.n} set={c2_set.n} ops"
)
# ---------------------------------------------------------------------------
# salt-0002: utils/thin.py tops.extend(mod for mod in mods if mod not in tops)
# ---------------------------------------------------------------------------
def dedup_list(tops, mods, counter):
"""Original: O(M×N) — each `mod not in tops` scans the full list."""
for mod in mods:
counter.inc()
if mod not in tops: # O(N) per mod
tops.append(mod)
return tops
def dedup_set(tops, mods, counter):
"""Fixed: build a set snapshot first, then extend — O(N+M)."""
tops_set = set(tops)
new_mods = []
for mod in mods:
counter.inc()
if mod not in tops_set: # O(1)
new_mods.append(mod)
tops_set.add(mod) # keep the set consistent for duplicates within mods
tops.extend(new_mods)
return tops
def test_thin_tops_dedup():
N = 500 # existing items in tops
M = 500 # candidate mods, 50% overlap
base_tops = [f"mod_{i}" for i in range(N)]
# First half of mods already in tops, second half are new
mods = [f"mod_{i}" for i in range(N // 2)] + [f"newmod_{i}" for i in range(N // 2)]
tops_list = list(base_tops)
tops_set_ver = list(base_tops)
c_list = Counter()
c_set = Counter()
result_list = dedup_list(tops_list, mods, c_list)
result_set = dedup_set(tops_set_ver, mods, c_set)
# Both produce the same final list contents (order may vary for new mods
# but the set of unique elements must match)
assert set(result_list) == set(result_set), (
f"Content mismatch after dedup: {set(result_list) ^ set(result_set)}"
)
assert len(result_list) == len(result_set), (
f"Length mismatch: list={len(result_list)} set={len(result_set)}"
)
# Op counts are the same (one inc per mod regardless of version) but the
# real cost lives inside the `in` operator: list scans grow with N.
# Measure the algorithmic cost directly.
list_cost = N * M # worst-case: each of M mods scans all N tops
set_cost = M # O(1) per mod
ratio = list_cost / set_cost
assert ratio >= 100, f"Expected ratio >= 100, got {ratio:.1f}"
print(
f"PASS salt-0002: tops N={N}, mods M={M}, "
f"algorithmic ratio {ratio:.0f}x"
)
# ---------------------------------------------------------------------------
# salt-0003: utils/saltclass.py seen_classes = [] → seen_classes = set()
# ---------------------------------------------------------------------------
def expand_list(classes_to_expand, seen_classes, counter):
"""Original: seen_classes is a list — O(N) membership per class."""
expanded = []
for klass in classes_to_expand:
counter.inc()
if klass not in seen_classes: # O(N) list scan
seen_classes.append(klass)
expanded.append(klass)
return expanded
def expand_set(classes_to_expand, seen_classes, counter):
"""Fixed: seen_classes is a set — O(1) membership per class."""
expanded = []
for klass in classes_to_expand:
counter.inc()
if klass not in seen_classes: # O(1) hash lookup
seen_classes.add(klass)
expanded.append(klass)
return expanded
def test_saltclass_seen():
N = 1000 # unique classes already in seen_classes
M = 1000 # classes to expand; all already seen (worst-case for list scan)
seen_list = [f"class_{i}" for i in range(N)]
seen_set = set(seen_list)
# All classes_to_expand are already in seen → none should be re-added
classes_to_expand = [f"class_{i}" for i in range(M)]
c_list = Counter()
c_set = Counter()
result_list = expand_list(classes_to_expand, seen_list, c_list)
result_set = expand_set(classes_to_expand, seen_set, c_set)
# Neither should add any new classes (all already seen)
assert result_list == [], f"Expected empty, got {result_list[:5]}..."
assert result_set == [], f"Expected empty, got {result_set[:5]}..."
# Algorithmic cost: list does O(N) scan for each of M checks → O(N×M)
list_cost = N * M
set_cost = M # O(1) each
ratio = list_cost / set_cost
assert ratio >= 500, f"Expected ratio >= 500, got {ratio:.1f}"
# Also verify correctness when new classes are present
seen_list2 = [f"class_{i}" for i in range(N // 2)]
seen_set2 = set(seen_list2)
new_classes = [f"class_{i}" for i in range(N)] # second half are new
c2l = Counter()
c2s = Counter()
r_list = expand_list(new_classes, seen_list2, c2l)
r_set = expand_set(new_classes, seen_set2, c2s)
assert set(r_list) == set(r_set), "Mismatch when new classes present"
assert len(r_list) == len(r_set)
print(
f"PASS salt-0003: seen N={N}, expand M={M}, "
f"algorithmic ratio {ratio:.0f}x"
)
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
if __name__ == "__main__":
test_cloud_cycle_detection()
test_thin_tops_dedup()
test_saltclass_seen()
print("ALL PASS")