package unit; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; /** * Unit tests for CWE-407 defects in Spring Framework. * * spring-0003 (HIGH): AbstractApplicationEventMulticaster.retrieveApplicationListeners() * builds allListeners as ArrayList. Inside the O(L) listenerBeans loop it calls * allListeners.contains() twice (lines 279, 285). Total O(L × (P+L)) = O(n²). * Hit on every event dispatch cache-miss. * * spring-0004 (MEDIUM): DefaultListenerRetriever.getApplicationListeners() has the * same pattern — ArrayList allListeners, allListeners.contains(listener) in loop * over applicationListenerBeans (line 512). * * Run: java -ea -cp . unit.SpringEventMulticasterTest */ public class SpringEventMulticasterTest { // ----------------------------------------------------------------------- // Listener model — identity-equal objects (like ApplicationListener instances) // ----------------------------------------------------------------------- static final class Listener { final String name; final boolean isProxy; final Listener target; // non-null if this is a proxy Listener(String name) { this.name = name; this.isProxy = false; this.target = null; } Listener(String name, Listener target) { this.name = name; this.isProxy = true; this.target = target; } @Override public String toString() { return (isProxy ? "Proxy(" : "Listener(") + name + ")"; } } // ----------------------------------------------------------------------- // spring-0003 model: retrieveApplicationListeners() — ArrayList vs LinkedHashSet // ----------------------------------------------------------------------- static final class DefectiveRetriever { long containsProbes = 0; /** Models the defective retrieveApplicationListeners() inner loop. */ List retrieve(List programmatic, List beanListeners) { // allListeners starts with programmatic listeners List allListeners = new ArrayList<>(programmatic); for (Listener[] pair : beanListeners) { // pair[0] = proxy, pair[1] = unwrapped target (or same if not proxy) Listener listener = pair[0]; Listener unwrapped = pair[1]; if (listener != unwrapped) { // Line 279: allListeners.contains(unwrappedListener) containsProbes += allListeners.size(); // O(n) scan if (allListeners.contains(unwrapped)) { allListeners.remove(unwrapped); allListeners.add(listener); continue; } } // Line 285: !allListeners.contains(listener) containsProbes += allListeners.size(); // O(n) scan if (!allListeners.contains(listener)) { allListeners.add(listener); } } return allListeners; } } static final class FixedRetriever { long containsProbes = 0; /** Models the fixed retrieveApplicationListeners() using LinkedHashSet. */ List retrieve(List programmatic, List beanListeners) { LinkedHashSet allListenerSet = new LinkedHashSet<>(programmatic); for (Listener[] pair : beanListeners) { Listener listener = pair[0]; Listener unwrapped = pair[1]; if (listener != unwrapped) { containsProbes += 1; // O(1) LinkedHashSet.remove() if (allListenerSet.remove(unwrapped)) { allListenerSet.add(listener); continue; } } containsProbes += 1; // O(1) LinkedHashSet.contains() if (!allListenerSet.contains(listener)) { allListenerSet.add(listener); } } return new ArrayList<>(allListenerSet); } } // ----------------------------------------------------------------------- // spring-0004 model: DefaultListenerRetriever.getApplicationListeners() // ----------------------------------------------------------------------- static final class DefectiveDefaultRetriever { long containsProbes = 0; List getListeners(List programmatic, List beanListeners) { List allListeners = new ArrayList<>(programmatic); for (Listener l : beanListeners) { containsProbes += allListeners.size(); // O(n) scan — line 512 if (!allListeners.contains(l)) { allListeners.add(l); } } return allListeners; } } static final class FixedDefaultRetriever { long containsProbes = 0; List getListeners(List programmatic, List beanListeners) { LinkedHashSet allListenerSet = new LinkedHashSet<>(programmatic); for (Listener l : beanListeners) { containsProbes += 1; // O(1) LinkedHashSet.add() allListenerSet.add(l); } return new ArrayList<>(allListenerSet); } } // ----------------------------------------------------------------------- // Test 1 — spring-0003 correctness: proxy replacement and dedup // ----------------------------------------------------------------------- static void test1_spring0003_correctness() { Listener a = new Listener("A"); Listener b = new Listener("B"); Listener bProxy = new Listener("B-proxy", b); // proxy wrapping b List programmatic = List.of(a, b); // beanListeners: a proxy for b (should replace b in the list) List beanListeners = new ArrayList<>(); beanListeners.add(new Listener[]{bProxy, b}); DefectiveRetriever def = new DefectiveRetriever(); FixedRetriever fix = new FixedRetriever(); List defResult = def.retrieve(programmatic, beanListeners); List fixResult = fix.retrieve(programmatic, beanListeners); assert defResult.size() == fixResult.size() : "spring-0003 correctness: size differs def=" + defResult.size() + " fix=" + fixResult.size(); assert defResult.equals(fixResult) : "spring-0003 correctness: listener lists differ: def=" + defResult + " fix=" + fixResult; // b should be replaced by bProxy assert !fixResult.contains(b) : "spring-0003 correctness: bare b should be replaced by proxy"; assert fixResult.contains(bProxy) : "spring-0003 correctness: bProxy should be present"; System.out.println("PASS test1_spring0003_correctness: proxy replacement correct, size=" + fixResult.size()); } // ----------------------------------------------------------------------- // Test 2 — spring-0003 complexity: O(n²) vs O(n) probes // ----------------------------------------------------------------------- static void test2_spring0003_complexity_ratio() { int small = 50; int large = 500; // Build inputs: P programmatic listeners, L bean listeners (all unique, no proxies) List smallProg = new ArrayList<>(), largeProg = new ArrayList<>(); List smallBean = new ArrayList<>(), largeBean = new ArrayList<>(); for (int i = 0; i < small; i++) { smallProg.add(new Listener("prog-" + i)); } for (int i = 0; i < small; i++) { Listener l = new Listener("bean-" + i); smallBean.add(new Listener[]{l, l}); // no proxy } for (int i = 0; i < large; i++) { largeProg.add(new Listener("prog-" + i)); } for (int i = 0; i < large; i++) { Listener l = new Listener("bean-" + i); largeBean.add(new Listener[]{l, l}); } DefectiveRetriever defSmall = new DefectiveRetriever(); DefectiveRetriever defLarge = new DefectiveRetriever(); FixedRetriever fixSmall = new FixedRetriever(); FixedRetriever fixLarge = new FixedRetriever(); defSmall.retrieve(smallProg, smallBean); defLarge.retrieve(largeProg, largeBean); fixSmall.retrieve(smallProg, smallBean); fixLarge.retrieve(largeProg, largeBean); double defRatio = (double) defLarge.containsProbes / defSmall.containsProbes; double fixRatio = (double) fixLarge.containsProbes / fixSmall.containsProbes; System.out.printf(" defective probes: small=%d large=%d ratio=%.1fx%n", defSmall.containsProbes, defLarge.containsProbes, defRatio); System.out.printf(" fixed probes: small=%d large=%d ratio=%.1fx%n", fixSmall.containsProbes, fixLarge.containsProbes, fixRatio); assert defRatio > 50.0 : "spring-0003 complexity: defective ratio should be >50x, got " + defRatio; assert fixRatio < 20.0 : "spring-0003 complexity: fixed ratio should be <20x, got " + fixRatio; System.out.printf("PASS test2_spring0003_complexity_ratio: defective=%.0fx fixed=%.0fx%n", defRatio, fixRatio); } // ----------------------------------------------------------------------- // Test 3 — spring-0003 absolute counts at N=200 // ----------------------------------------------------------------------- static void test3_spring0003_absolute_counts() { int P = 100, L = 100; // 100 programmatic + 100 bean listeners, all unique List prog = new ArrayList<>(); List bean = new ArrayList<>(); for (int i = 0; i < P; i++) prog.add(new Listener("prog-" + i)); for (int i = 0; i < L; i++) { Listener l = new Listener("bean-" + i); bean.add(new Listener[]{l, l}); } DefectiveRetriever def = new DefectiveRetriever(); FixedRetriever fix = new FixedRetriever(); def.retrieve(prog, bean); fix.retrieve(prog, bean); // Defective: each of L iterations checks list of size (P+0..L-1) twice // Lower bound: L * P probes (at minimum the initial P listeners are scanned) long expectedDefMin = (long) L * P; assert def.containsProbes >= expectedDefMin : "spring-0003 counts: defective probes=" + def.containsProbes + " expected>=" + expectedDefMin; // Fixed: exactly L probes (one per bean listener) assert fix.containsProbes == L : "spring-0003 counts: fixed probes=" + fix.containsProbes + " expected=" + L; long speedup = def.containsProbes / fix.containsProbes; System.out.printf("PASS test3_spring0003_absolute_counts: defective=%d fixed=%d speedup=%dx%n", def.containsProbes, fix.containsProbes, speedup); } // ----------------------------------------------------------------------- // Test 4 — spring-0004 correctness // ----------------------------------------------------------------------- static void test4_spring0004_correctness() { Listener a = new Listener("A"), b = new Listener("B"), c = new Listener("C"); List programmatic = List.of(a, b); List beanListeners = List.of(b, c); // b is duplicate DefectiveDefaultRetriever def = new DefectiveDefaultRetriever(); FixedDefaultRetriever fix = new FixedDefaultRetriever(); List defResult = def.getListeners(programmatic, beanListeners); List fixResult = fix.getListeners(programmatic, beanListeners); assert defResult.size() == 3 : "spring-0004 correctness: defective size=" + defResult.size() + " expected=3"; assert fixResult.size() == 3 : "spring-0004 correctness: fixed size=" + fixResult.size() + " expected=3"; assert defResult.equals(fixResult) : "spring-0004 correctness: results differ def=" + defResult + " fix=" + fixResult; System.out.println("PASS test4_spring0004_correctness: size=3, dedup correct"); } // ----------------------------------------------------------------------- // Test 5 — spring-0004 complexity ratio // ----------------------------------------------------------------------- static void test5_spring0004_complexity_ratio() { int small = 50, large = 500; List smallProg = new ArrayList<>(), largeProg = new ArrayList<>(); List smallBean = new ArrayList<>(), largeBean = new ArrayList<>(); for (int i = 0; i < small; i++) smallProg.add(new Listener("p" + i)); for (int i = 0; i < small; i++) smallBean.add(new Listener("b" + i)); for (int i = 0; i < large; i++) largeProg.add(new Listener("p" + i)); for (int i = 0; i < large; i++) largeBean.add(new Listener("b" + i)); DefectiveDefaultRetriever defSmall = new DefectiveDefaultRetriever(); DefectiveDefaultRetriever defLarge = new DefectiveDefaultRetriever(); FixedDefaultRetriever fixSmall = new FixedDefaultRetriever(); FixedDefaultRetriever fixLarge = new FixedDefaultRetriever(); defSmall.getListeners(smallProg, smallBean); defLarge.getListeners(largeProg, largeBean); fixSmall.getListeners(smallProg, smallBean); fixLarge.getListeners(largeProg, largeBean); double defRatio = (double) defLarge.containsProbes / defSmall.containsProbes; double fixRatio = (double) fixLarge.containsProbes / fixSmall.containsProbes; System.out.printf(" defective probes: small=%d large=%d ratio=%.1fx%n", defSmall.containsProbes, defLarge.containsProbes, defRatio); System.out.printf(" fixed probes: small=%d large=%d ratio=%.1fx%n", fixSmall.containsProbes, fixLarge.containsProbes, fixRatio); assert defRatio > 50.0 : "spring-0004 complexity: defective ratio should be >50x, got " + defRatio; assert fixRatio < 20.0 : "spring-0004 complexity: fixed ratio should be <20x, got " + fixRatio; System.out.printf("PASS test5_spring0004_complexity_ratio: defective=%.0fx fixed=%.0fx%n", defRatio, fixRatio); } // ----------------------------------------------------------------------- // Test 6 — spring-0004 absolute counts at N=200 // ----------------------------------------------------------------------- static void test6_spring0004_absolute_counts() { int P = 100, L = 100; List prog = new ArrayList<>(), bean = new ArrayList<>(); for (int i = 0; i < P; i++) prog.add(new Listener("p" + i)); for (int i = 0; i < L; i++) bean.add(new Listener("b" + i)); // all unique DefectiveDefaultRetriever def = new DefectiveDefaultRetriever(); FixedDefaultRetriever fix = new FixedDefaultRetriever(); def.getListeners(prog, bean); fix.getListeners(prog, bean); long expectedDefMin = (long) L * P; assert def.containsProbes >= expectedDefMin : "spring-0004 counts: defective probes=" + def.containsProbes + " expected>=" + expectedDefMin; assert fix.containsProbes == L : "spring-0004 counts: fixed probes=" + fix.containsProbes + " expected=" + L; System.out.printf("PASS test6_spring0004_absolute_counts: defective=%d fixed=%d speedup=%dx%n", def.containsProbes, fix.containsProbes, def.containsProbes / fix.containsProbes); } // ----------------------------------------------------------------------- // main // ----------------------------------------------------------------------- public static void main(String[] args) { System.out.println("=== SpringEventMulticasterTest — CWE-407 spring-0003 / spring-0004 ==="); int passed = 0, failed = 0; Runnable[] tests = { SpringEventMulticasterTest::test1_spring0003_correctness, SpringEventMulticasterTest::test2_spring0003_complexity_ratio, SpringEventMulticasterTest::test3_spring0003_absolute_counts, SpringEventMulticasterTest::test4_spring0004_correctness, SpringEventMulticasterTest::test5_spring0004_complexity_ratio, SpringEventMulticasterTest::test6_spring0004_absolute_counts, }; for (Runnable test : tests) { try { test.run(); passed++; } catch (AssertionError e) { System.out.println("FAIL: " + e.getMessage()); failed++; } } System.out.println("---"); System.out.println("Results: " + passed + " passed, " + failed + " failed"); if (failed > 0) System.exit(1); } }