java-topology/defects/spring/unit/SpringEventMulticasterTest.java
russell@unturf.com 9934133dcf whitepaper: 312 sites / 151 ecosystems — wave2+3 defect tables and PDF rebuild
Add 88 new defect entries to HIGH and MEDIUM tables:
  HIGH: mysql-0001/0002, mariadb-0001, redis-0001/0002, valkey-0001/0002, openvpn-0001,
        vlc-0001, prometheus-0001, otel-collector-0001, cockroachdb-0001..0004,
        tidb-0001..0008, kubernetes-0001/0002, go-0001, kotlin-0002, scala-0001,
        allegro5-0001, sdl2-0001, grafana-0001, clickhouse-0001, duckdb-0001,
        mongodb-0001, envoy-0001, istio-0001, cilium-0001, linkerd2-0001,
        linux-0001/0002/0003, tor-0002/0003, curl-0001, julia-0001, lua-0001,
        perl5-0001, nats-0001, spring-0003/0004, tomcat-0001, onos-0002, odl-0002

  MEDIUM: helm-0001, mariadb-0002, openssl-0001/0002, memcached-0001,
          cassandra-0001..0004, flink-0001, storm-0001/0002, zookeeper-0001..0003,
          pip-0001, gradle-0001, nginx-0001, haproxy-0001, caddy-0001, varnish-0001,
          ffmpeg-0001, gstreamer-0001, raylib-0001, love2d-0001, php-0001/0002,
          r-source-0001, cpython-0002, ruby-0001, rabbitmq-0003/0004, activemq-0001,
          ovs-0001, onos-0003, odl-0002, jetty-0001

PDF: 976K
2026-03-27 15:23:43 -04:00

370 lines
17 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.

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<Listener> retrieve(List<Listener> programmatic, List<Listener[]> beanListeners) {
// allListeners starts with programmatic listeners
List<Listener> 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<Listener> retrieve(List<Listener> programmatic, List<Listener[]> beanListeners) {
LinkedHashSet<Listener> 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<Listener> getListeners(List<Listener> programmatic, List<Listener> beanListeners) {
List<Listener> 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<Listener> getListeners(List<Listener> programmatic, List<Listener> beanListeners) {
LinkedHashSet<Listener> 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<Listener> programmatic = List.of(a, b);
// beanListeners: a proxy for b (should replace b in the list)
List<Listener[]> beanListeners = new ArrayList<>();
beanListeners.add(new Listener[]{bProxy, b});
DefectiveRetriever def = new DefectiveRetriever();
FixedRetriever fix = new FixedRetriever();
List<Listener> defResult = def.retrieve(programmatic, beanListeners);
List<Listener> 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<Listener> smallProg = new ArrayList<>(), largeProg = new ArrayList<>();
List<Listener[]> 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<Listener> prog = new ArrayList<>();
List<Listener[]> 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<Listener> programmatic = List.of(a, b);
List<Listener> beanListeners = List.of(b, c); // b is duplicate
DefectiveDefaultRetriever def = new DefectiveDefaultRetriever();
FixedDefaultRetriever fix = new FixedDefaultRetriever();
List<Listener> defResult = def.getListeners(programmatic, beanListeners);
List<Listener> 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<Listener> smallProg = new ArrayList<>(), largeProg = new ArrayList<>();
List<Listener> 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<Listener> 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);
}
}