java-topology/defects/argo-cd/unit/ArgoCdTest.java
russell@unturf.com 05e6aace95 istio/argo-cd: CWE-407 findings
istio-0001: gateway reportGatewayStatus addressesToReport dedup O(I^2)
  pilot/pkg/config/kube/gateway/conversion.go +
  pilot/pkg/config/kube/agentgateway/gateway_status.go
  Fix: map[string]struct{} seen-set replaces slices.Contains on growing list

argo-cd-0001: mergeIgnoreDifferences O(P^2) per field type
  util/argo/diff/ignore.go
  Fix: pre-compute sets for JQPathExpressions/JSONPointers/ManagedFieldsManagers

Both: 2/2 unit tests PASS; 3.4x and 3.7x speedup measured
2026-03-30 09:53:12 -04:00

159 lines
7.2 KiB
Java
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.

import java.util.*;
/**
* Unit test simulating ArgoCD CWE-407 defect:
* argo-cd-0001: mergeIgnoreDifferences O(P^2) per field type
*
* In util/argo/diff/ignore.go, mergeIgnoreDifferences merges three lists
* (JQPathExpressions, JSONPointers, ManagedFieldsManagers) from a source IgnoreDifference
* into a target, skipping duplicates. The dedup is performed with slices.Contains on the
* growing target slice, giving O(P^2) per field where P = total paths accumulated.
*
* HasIgnoreDifference() calls mergeIgnoreDifferences once per matching ResourceIgnoreDifferences
* entry in the ArgoCD config, and is invoked for EVERY managed resource during sync diff
* computation. With many ignore-diff rules (e.g. 50) each carrying 50 paths, a single
* reconciliation loop pays O(50^2) = 2 500 comparisons per resource instead of O(50).
*
* Fix: pre-compute a map[string]struct{} for each target field before the merge loop,
* reducing each merge to O(P) amortized.
*/
public class ArgoCdTest {
// --- Defect simulation ---
static List<String> mergeListDefect(List<String> target, List<String> from) {
List<String> result = new ArrayList<>(target);
for (String v : from) {
if (!result.contains(v)) { // O(N) scan on growing list
result.add(v);
}
}
return result;
}
static void mergeIgnoreDifferencesDefect(
List<String> targetJQ, List<String> fromJQ,
List<String> targetJP, List<String> fromJP,
List<String> targetMF, List<String> fromMF) {
// Simulate three field merges each O(P^2)
targetJQ.addAll(mergeListDefect(new ArrayList<>(), fromJQ).stream()
.filter(v -> !targetJQ.contains(v)).toList());
for (String v : fromJQ) {
if (!targetJQ.contains(v)) targetJQ.add(v);
}
for (String v : fromJP) {
if (!targetJP.contains(v)) targetJP.add(v);
}
for (String v : fromMF) {
if (!targetMF.contains(v)) targetMF.add(v);
}
}
// --- Fixed simulation ---
static void mergeIgnoreDifferencesFixed(
List<String> targetJQ, List<String> fromJQ,
List<String> targetJP, List<String> fromJP,
List<String> targetMF, List<String> fromMF) {
Set<String> jqSet = new HashSet<>(targetJQ);
Set<String> jpSet = new HashSet<>(targetJP);
Set<String> mfSet = new HashSet<>(targetMF);
for (String v : fromJQ) { if (jqSet.add(v)) targetJQ.add(v); }
for (String v : fromJP) { if (jpSet.add(v)) targetJP.add(v); }
for (String v : fromMF) { if (mfSet.add(v)) targetMF.add(v); }
}
// --- Helpers ---
static List<String> makePathList(String prefix, int count) {
List<String> list = new ArrayList<>(count);
for (int i = 0; i < count; i++) list.add(prefix + i);
return list;
}
// Simulate HasIgnoreDifference merging R matching ignore configs, each with P paths
static long benchmarkDefect(int ignoreRules, int pathsPerRule) {
long elapsed = 0;
int ITERS = 500;
for (int iter = 0; iter < ITERS; iter++) {
List<String> accJQ = new ArrayList<>();
List<String> accJP = new ArrayList<>();
List<String> accMF = new ArrayList<>();
long t0 = System.nanoTime();
for (int r = 0; r < ignoreRules; r++) {
List<String> fromJQ = makePathList(".spec.field" + r + "[", pathsPerRule);
List<String> fromJP = makePathList("/spec/field" + r + "/", pathsPerRule);
List<String> fromMF = makePathList("manager-" + r + "-", pathsPerRule);
mergeIgnoreDifferencesDefect(accJQ, fromJQ, accJP, fromJP, accMF, fromMF);
}
elapsed += System.nanoTime() - t0;
}
return elapsed;
}
static long benchmarkFixed(int ignoreRules, int pathsPerRule) {
long elapsed = 0;
int ITERS = 500;
for (int iter = 0; iter < ITERS; iter++) {
List<String> accJQ = new ArrayList<>();
List<String> accJP = new ArrayList<>();
List<String> accMF = new ArrayList<>();
long t0 = System.nanoTime();
for (int r = 0; r < ignoreRules; r++) {
List<String> fromJQ = makePathList(".spec.field" + r + "[", pathsPerRule);
List<String> fromJP = makePathList("/spec/field" + r + "/", pathsPerRule);
List<String> fromMF = makePathList("manager-" + r + "-", pathsPerRule);
mergeIgnoreDifferencesFixed(accJQ, fromJQ, accJP, fromJP, accMF, fromMF);
}
elapsed += System.nanoTime() - t0;
}
return elapsed;
}
public static void main(String[] args) {
// Correctness: merged results must be identical
List<String> tJQ = new ArrayList<>(Arrays.asList("jq0", "jq1"));
List<String> tJP = new ArrayList<>(Arrays.asList("jp0"));
List<String> tMF = new ArrayList<>(Arrays.asList("mgr0"));
List<String> fJQ = Arrays.asList("jq1", "jq2", "jq3"); // jq1 is dup
List<String> fJP = Arrays.asList("jp0", "jp1"); // jp0 is dup
List<String> fMF = Arrays.asList("mgr1", "mgr0"); // mgr0 is dup
List<String> dJQ = new ArrayList<>(tJQ), dJP = new ArrayList<>(tJP), dMF = new ArrayList<>(tMF);
List<String> xJQ = new ArrayList<>(tJQ), xJP = new ArrayList<>(tJP), xMF = new ArrayList<>(tMF);
mergeIgnoreDifferencesDefect(dJQ, fJQ, dJP, fJP, dMF, fMF);
mergeIgnoreDifferencesFixed(xJQ, fJQ, xJP, fJP, xMF, fMF);
System.out.println("Defect JQ: " + dJQ + " Fixed JQ: " + xJQ);
System.out.println("Defect JP: " + dJP + " Fixed JP: " + xJP);
System.out.println("Defect MF: " + dMF + " Fixed MF: " + xMF);
assert new HashSet<>(dJQ).equals(new HashSet<>(xJQ)) : "FAIL: JQ differs: " + dJQ + " vs " + xJQ;
assert new HashSet<>(dJP).equals(new HashSet<>(xJP)) : "FAIL: JP differs";
assert new HashSet<>(dMF).equals(new HashSet<>(xMF)) : "FAIL: MF differs";
assert dJQ.size() == 4 : "FAIL: expected 4 unique JQ paths, got " + dJQ.size();
assert dJP.size() == 2 : "FAIL: expected 2 unique JP paths, got " + dJP.size();
assert dMF.size() == 2 : "FAIL: expected 2 unique MF managers, got " + dMF.size();
System.out.println("Correctness: PASS");
// Benchmark: 20 ignore rules, 20 paths each (realistic large ArgoCD config)
int rules = 20, paths = 20;
// Warmup
benchmarkDefect(rules, paths); benchmarkFixed(rules, paths);
long defectNs = benchmarkDefect(rules, paths);
long fixedNs = benchmarkFixed(rules, paths);
if (fixedNs < 100_000) fixedNs = 100_000;
double ratio = (double) defectNs / fixedNs;
System.out.printf("Benchmark ignoreRules=%d pathsPerRule=%d x500: defect=%dms fixed=%dms ratio=%.1fx%n",
rules, paths, defectNs / 1_000_000, fixedNs / 1_000_000, ratio);
assert ratio > 1.5 : "FAIL: expected defect to be measurably slower (got " + ratio + "x)";
System.out.println("Performance regression: PASS (ratio=" + String.format("%.1f", ratio) + "x)");
System.out.println("ALL PASS");
}
}