diff --git a/defects/argo-cd/patch/argo-cd-0001-merge-ignore-diff-dedup-on2.patch b/defects/argo-cd/patch/argo-cd-0001-merge-ignore-diff-dedup-on2.patch new file mode 100644 index 000000000..9d220dea7 --- /dev/null +++ b/defects/argo-cd/patch/argo-cd-0001-merge-ignore-diff-dedup-on2.patch @@ -0,0 +1,48 @@ +# UNDF: UNDF-2026-000000760 +# UNDF: (leave blank) +--- a/util/argo/diff/ignore.go ++++ b/util/argo/diff/ignore.go +@@ -89,18 +89,36 @@ func resourceToIgnoreDifference(resource v1alpha1.ResourceIgnoreDifferences) *I + // mergeIgnoreDifferences will merge all ignores in the given from in target + // skipping repeated configs. + func mergeIgnoreDifferences(from *IgnoreDifference, target *IgnoreDifference) { +- for _, jqPath := range from.JQPathExpressions { +- if !slices.Contains(target.JQPathExpressions, jqPath) { ++ // Pre-compute sets for O(1) membership checks instead of O(N) slices.Contains ++ // on the growing target slice (fixes O(P^2) → O(P) per field). ++ jqSet := make(map[string]struct{}, len(target.JQPathExpressions)) ++ for _, v := range target.JQPathExpressions { ++ jqSet[v] = struct{}{} ++ } ++ jpSet := make(map[string]struct{}, len(target.JSONPointers)) ++ for _, v := range target.JSONPointers { ++ jpSet[v] = struct{}{} ++ } ++ mfSet := make(map[string]struct{}, len(target.ManagedFieldsManagers)) ++ for _, v := range target.ManagedFieldsManagers { ++ mfSet[v] = struct{}{} ++ } ++ ++ for _, jqPath := range from.JQPathExpressions { ++ if _, exists := jqSet[jqPath]; !exists { + target.JQPathExpressions = append(target.JQPathExpressions, jqPath) ++ jqSet[jqPath] = struct{}{} + } + } +- for _, jsonPointer := range from.JSONPointers { +- if !slices.Contains(target.JSONPointers, jsonPointer) { ++ for _, jsonPointer := range from.JSONPointers { ++ if _, exists := jpSet[jsonPointer]; !exists { + target.JSONPointers = append(target.JSONPointers, jsonPointer) ++ jpSet[jsonPointer] = struct{}{} + } + } +- for _, manager := range from.ManagedFieldsManagers { +- if !slices.Contains(target.ManagedFieldsManagers, manager) { ++ for _, manager := range from.ManagedFieldsManagers { ++ if _, exists := mfSet[manager]; !exists { + target.ManagedFieldsManagers = append(target.ManagedFieldsManagers, manager) ++ mfSet[manager] = struct{}{} + } + } + } diff --git a/defects/argo-cd/unit/ArgoCdTest.class b/defects/argo-cd/unit/ArgoCdTest.class new file mode 100644 index 000000000..1f06ce26a Binary files /dev/null and b/defects/argo-cd/unit/ArgoCdTest.class differ diff --git a/defects/argo-cd/unit/ArgoCdTest.java b/defects/argo-cd/unit/ArgoCdTest.java new file mode 100644 index 000000000..1b59144bb --- /dev/null +++ b/defects/argo-cd/unit/ArgoCdTest.java @@ -0,0 +1,159 @@ +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 mergeListDefect(List target, List from) { + List 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 targetJQ, List fromJQ, + List targetJP, List fromJP, + List targetMF, List 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 targetJQ, List fromJQ, + List targetJP, List fromJP, + List targetMF, List fromMF) { + Set jqSet = new HashSet<>(targetJQ); + Set jpSet = new HashSet<>(targetJP); + Set 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 makePathList(String prefix, int count) { + List 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 accJQ = new ArrayList<>(); + List accJP = new ArrayList<>(); + List accMF = new ArrayList<>(); + long t0 = System.nanoTime(); + for (int r = 0; r < ignoreRules; r++) { + List fromJQ = makePathList(".spec.field" + r + "[", pathsPerRule); + List fromJP = makePathList("/spec/field" + r + "/", pathsPerRule); + List 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 accJQ = new ArrayList<>(); + List accJP = new ArrayList<>(); + List accMF = new ArrayList<>(); + long t0 = System.nanoTime(); + for (int r = 0; r < ignoreRules; r++) { + List fromJQ = makePathList(".spec.field" + r + "[", pathsPerRule); + List fromJP = makePathList("/spec/field" + r + "/", pathsPerRule); + List 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 tJQ = new ArrayList<>(Arrays.asList("jq0", "jq1")); + List tJP = new ArrayList<>(Arrays.asList("jp0")); + List tMF = new ArrayList<>(Arrays.asList("mgr0")); + + List fJQ = Arrays.asList("jq1", "jq2", "jq3"); // jq1 is dup + List fJP = Arrays.asList("jp0", "jp1"); // jp0 is dup + List fMF = Arrays.asList("mgr1", "mgr0"); // mgr0 is dup + + List dJQ = new ArrayList<>(tJQ), dJP = new ArrayList<>(tJP), dMF = new ArrayList<>(tMF); + List 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"); + } +} diff --git a/defects/istio/patch/istio-0001-gateway-address-dedup-on2.patch b/defects/istio/patch/istio-0001-gateway-address-dedup-on2.patch new file mode 100644 index 000000000..f030cd225 --- /dev/null +++ b/defects/istio/patch/istio-0001-gateway-address-dedup-on2.patch @@ -0,0 +1,54 @@ +# UNDF: UNDF-2026-000000114 +# UNDF: (leave blank) +--- a/pilot/pkg/config/kube/gateway/conversion.go ++++ b/pilot/pkg/config/kube/gateway/conversion.go +@@ -1783,10 +1783,16 @@ func reportGatewayStatus( + if wantAddressType != k8s.HostnameAddressType { + addressesToReport = internalIP + } + if wantAddressType != k8s.IPAddressType { ++ pendingSet := make(map[string]struct{}, len(pending)) ++ for _, p := range pending { ++ pendingSet[p] = struct{}{} ++ } ++ seenHosts := make(map[string]struct{}, len(internalIP)) ++ for _, h := range addressesToReport { ++ seenHosts[h] = struct{}{} ++ } + for _, hostport := range internal { + svchost, _, _ := net.SplitHostPort(hostport) +- if !slices.Contains(pending, svchost) && !slices.Contains(addressesToReport, svchost) { ++ if _, isPending := pendingSet[svchost]; !isPending { ++ if _, isSeen := seenHosts[svchost]; !isSeen { + addressesToReport = append(addressesToReport, svchost) ++ seenHosts[svchost] = struct{}{} ++ } + } + } + } +--- a/pilot/pkg/config/kube/agentgateway/gateway_status.go ++++ b/pilot/pkg/config/kube/agentgateway/gateway_status.go +@@ -148,10 +148,16 @@ func reportGatewayStatus( + if wantAddressType != gatewayv1.HostnameAddressType { + addressesToReport = internalIP + } + if wantAddressType != gatewayv1.IPAddressType { ++ pendingSet := make(map[string]struct{}, len(pending)) ++ for _, p := range pending { ++ pendingSet[p] = struct{}{} ++ } ++ seenHosts := make(map[string]struct{}, len(internalIP)) ++ for _, h := range addressesToReport { ++ seenHosts[h] = struct{}{} ++ } + for _, hostport := range internal { + svchost, _, _ := net.SplitHostPort(hostport) +- if !slices.Contains(pending, svchost) && !slices.Contains(addressesToReport, svchost) { ++ if _, isPending := pendingSet[svchost]; !isPending { ++ if _, isSeen := seenHosts[svchost]; !isSeen { + addressesToReport = append(addressesToReport, svchost) ++ seenHosts[svchost] = struct{}{} ++ } + } + } + } diff --git a/defects/istio/unit/IstioTest.class b/defects/istio/unit/IstioTest.class new file mode 100644 index 000000000..a6ab6128a Binary files /dev/null and b/defects/istio/unit/IstioTest.class differ diff --git a/defects/istio/unit/IstioTest.java b/defects/istio/unit/IstioTest.java index 99341e128..43c44a437 100644 --- a/defects/istio/unit/IstioTest.java +++ b/defects/istio/unit/IstioTest.java @@ -1,171 +1,120 @@ -package unit; import java.util.*; /** - * IstioTest — CWE-407 benchmark for istio-0001 + * Unit test simulating Istio CWE-407 defect: + * istio-0001: reportGatewayStatus addressesToReport dedup * - * istio-0001: virtualHostMatch slices.Contains(vh.Domains, domainName) - * called inside VirtualHost × patch nested loop → O(VH × P × D) + * In pilot/pkg/config/kube/gateway/conversion.go (and agentgateway/gateway_status.go), + * the function builds addressesToReport by iterating over 'internal' hostnames and + * checking slices.Contains(addressesToReport, svchost) on a list that grows each iteration. * - * Model: - * VH = number of VirtualHosts in a route config - * P = number of EnvoyFilter patches - * D = number of domain aliases per VirtualHost - * - * SLOW: for each VH, for each patch, slices.Contains(vh.domains) → O(VH × P × D) - * FAST: build domain→VH map once, O(VH×D) setup, then O(VH×P) matching → O(VH×P) + * Complexity: O(I^2) where I = number of internal gateway hostnames. + * Fix: use a map[string]struct{} seen-set for O(1) membership checks. */ public class IstioTest { - // ------------------------------------------------------------------------- - // Data model - // ------------------------------------------------------------------------- - static class VirtualHost { - final String name; - final List domains; - VirtualHost(String name, int domainCount) { - this.name = name; - this.domains = new ArrayList<>(domainCount); - // e.g. "svc.ns.svc.cluster.local", "svc.ns", "svc", "svc:80", ... - for (int i = 0; i < domainCount; i++) { - domains.add(name + "-alias-" + i); - } - // last domain is the canonical one we'll match against - domains.add(name + ".canonical"); - } - } - - static class Patch { - final String matchDomainName; - Patch(String domainName) { this.matchDomainName = domainName; } - } - - // ------------------------------------------------------------------------- - // SLOW: slices.Contains per virtualHostMatch call - // ------------------------------------------------------------------------- - static long patchRouteConfig_slow(List virtualHosts, List patches) { - long ops = 0; - for (VirtualHost vh : virtualHosts) { - for (Patch p : patches) { - // virtualHostMatch: slices.Contains(vh.domains, p.matchDomainName) - if (!p.matchDomainName.isEmpty()) { - for (String d : vh.domains) { - ops++; - if (d.equals(p.matchDomainName)) break; - } - } + /** Defect version: O(I^2) - slices.Contains on growing list */ + static List reportAddressesDefect(List internal, List pending) { + List addressesToReport = new ArrayList<>(); + for (String hostport : internal) { + String svchost = splitHost(hostport); + // O(P) scan on pending + O(A) scan on growing addressesToReport + if (!pending.contains(svchost) && !addressesToReport.contains(svchost)) { + addressesToReport.add(svchost); } } - return ops; + return addressesToReport; } - // ------------------------------------------------------------------------- - // FAST: domain→VH map built once before the loop - // ------------------------------------------------------------------------- - static long patchRouteConfig_fast(List virtualHosts, List patches) { - long ops = 0; - // Build index: O(VH × D) — counted once - Map domainIndex = new HashMap<>(); - for (VirtualHost vh : virtualHosts) { - for (String d : vh.domains) { - ops++; - domainIndex.put(d, vh); + /** Fixed version: O(I) - map-based seen set for O(1) lookups */ + static List reportAddressesFixed(List internal, List pending) { + Set pendingSet = new HashSet<>(pending); + Set seenHosts = new HashSet<>(); + List addressesToReport = new ArrayList<>(); + for (String hostport : internal) { + String svchost = splitHost(hostport); + if (!pendingSet.contains(svchost) && seenHosts.add(svchost)) { + addressesToReport.add(svchost); } } - // Now matching: O(1) per lookup - for (VirtualHost vh : virtualHosts) { - for (Patch p : patches) { - if (!p.matchDomainName.isEmpty()) { - ops++; // map.get — O(1) - domainIndex.get(p.matchDomainName); - } - } + return addressesToReport; + } + + static String splitHost(String hostport) { + int colon = hostport.lastIndexOf(':'); + if (colon > 0) return hostport.substring(0, colon); + return hostport; + } + + static long benchmarkDefect(int n) { + List internal = new ArrayList<>(); + List pending = new ArrayList<>(); + // Simulate n internal gateway hostnames (e.g. istio-ingressgateway.istio-system.svc.cluster.local:80) + for (int i = 0; i < n; i++) { + internal.add("gw-svc-" + i + ".istio-system.svc.cluster.local:80"); } - return ops; - } - - // ------------------------------------------------------------------------- - // Helpers - // ------------------------------------------------------------------------- - static List makeVirtualHosts(int count, int domainsEach) { - List list = new ArrayList<>(count); - for (int i = 0; i < count; i++) { - list.add(new VirtualHost("svc-" + i, domainsEach)); + long start = System.nanoTime(); + for (int iter = 0; iter < 200; iter++) { + reportAddressesDefect(internal, pending); } - return list; + return System.nanoTime() - start; } - static List makePatches(int count, List vhs) { - List patches = new ArrayList<>(count); - for (int i = 0; i < count; i++) { - // each patch targets the canonical domain of some VH - String target = vhs.get(i % vhs.size()).name + ".canonical"; - patches.add(new Patch(target)); + static long benchmarkFixed(int n) { + List internal = new ArrayList<>(); + List pending = new ArrayList<>(); + for (int i = 0; i < n; i++) { + internal.add("gw-svc-" + i + ".istio-system.svc.cluster.local:80"); } - return patches; + long start = System.nanoTime(); + for (int iter = 0; iter < 200; iter++) { + reportAddressesFixed(internal, pending); + } + return System.nanoTime() - start; } - static void bench(String label, long sOps, long fOps) { - System.out.printf(" %-55s slow=%9d fast=%7d ratio=%5.1fx%n", - label, sOps, fOps, (double) sOps / Math.max(fOps, 1)); - } - - // ------------------------------------------------------------------------- - // Main - // ------------------------------------------------------------------------- public static void main(String[] args) { - System.out.println("IstioTest — CWE-407 istio-0001 virtualHostMatch domain linear scan"); - System.out.println(); + // Correctness: both versions must return same result + List internal = Arrays.asList( + "gw-a.istio-system.svc.cluster.local:80", + "gw-b.istio-system.svc.cluster.local:80", + "gw-a.istio-system.svc.cluster.local:443", // duplicate host, different port + "gw-c.istio-system.svc.cluster.local:80" + ); + List pending = Arrays.asList("gw-c.istio-system.svc.cluster.local"); - // --- VH=100, P=5, D=10 --- - { - int VH = 100, P = 5, D = 10; - List vhs = makeVirtualHosts(VH, D); - List patches = makePatches(P, vhs); - long sOps = patchRouteConfig_slow(vhs, patches); - long fOps = patchRouteConfig_fast(vhs, patches); - bench("VH=100 P=5 D=10", sOps, fOps); - assert sOps > fOps * 2 : - "Expected slow >> fast, got slow=" + sOps + " fast=" + fOps; - } + List defectResult = reportAddressesDefect(internal, pending); + List fixedResult = reportAddressesFixed(internal, pending); - // --- VH=500, P=20, D=15 --- - { - int VH = 500, P = 20, D = 15; - List vhs = makeVirtualHosts(VH, D); - List patches = makePatches(P, vhs); - long sOps = patchRouteConfig_slow(vhs, patches); - long fOps = patchRouteConfig_fast(vhs, patches); - bench("VH=500 P=20 D=15", sOps, fOps); - assert sOps > fOps * 5 : - "Expected slow >> fast, got slow=" + sOps + " fast=" + fOps; - } + System.out.println("Defect result: " + defectResult); + System.out.println("Fixed result: " + fixedResult); + assert defectResult.equals(fixedResult) : "FAIL: results differ: " + defectResult + " vs " + fixedResult; + System.out.println("Correctness: PASS"); - // --- VH=1000, P=50, D=20 (large mesh) --- - { - int VH = 1000, P = 50, D = 20; - List vhs = makeVirtualHosts(VH, D); - List patches = makePatches(P, vhs); - long sOps = patchRouteConfig_slow(vhs, patches); - long fOps = patchRouteConfig_fast(vhs, patches); - bench("VH=1000 P=50 D=20 (large mesh)", sOps, fOps); - assert sOps > fOps * 10 : - "Expected slow >> fast, got slow=" + sOps + " fast=" + fOps; - } + // Both should contain gw-a and gw-b, but NOT gw-c (pending) + assert defectResult.contains("gw-a.istio-system.svc.cluster.local") : "FAIL: missing gw-a"; + assert defectResult.contains("gw-b.istio-system.svc.cluster.local") : "FAIL: missing gw-b"; + assert !defectResult.contains("gw-c.istio-system.svc.cluster.local") : "FAIL: gw-c should be excluded (pending)"; + assert defectResult.size() == 2 : "FAIL: expected 2 unique non-pending hosts, got " + defectResult.size(); + System.out.println("Exclusion of pending and dedup: PASS"); - // --- VH=2000, P=100, D=25 (stress) --- - { - int VH = 2000, P = 100, D = 25; - List vhs = makeVirtualHosts(VH, D); - List patches = makePatches(P, vhs); - long sOps = patchRouteConfig_slow(vhs, patches); - long fOps = patchRouteConfig_fast(vhs, patches); - bench("VH=2000 P=100 D=25 (stress)", sOps, fOps); - assert sOps > fOps * 10 : - "Expected slow >> fast, got slow=" + sOps + " fast=" + fOps; - } + // Benchmark at I=500 (many gateway replicas/ports across multiple gateway services) + int n = 500; + // Warmup + benchmarkDefect(n); benchmarkFixed(n); - System.out.println(); - System.out.println("All assertions passed."); + long defectNs = benchmarkDefect(n); + long fixedNs = benchmarkFixed(n); + // Ensure fixed is not too short to measure + if (fixedNs < 1_000_000) fixedNs = 1_000_000; + + double ratio = (double) defectNs / fixedNs; + System.out.printf("Benchmark n=%d x200: defect=%dms fixed=%dms ratio=%.1fx%n", + n, 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"); } }