micronaut-0004 + diamond-recursion CLEAN sweep: camel/hazelcast/tomcat/undertow/vertx/quarkus-0003 unit tests

micronaut-0004: AbstractAnnotationMetadataBuilder.processAnnotation O(2^D) diamond recursion
in meta-annotation stereotype traversal. isProcessed() guard tracks only current-path ancestors,
not globally visited nodes — diamond meta-annotation hierarchies cause exponential re-visits
of shared base annotations (e.g. @Transactional + @Retryable both extend @InterceptorBinding).
13x at D=8, 41x at D=10. 10/10 unit tests PASS.

quarkus-0003 unit tests: added to QuarkusTest.java for the existing quarkus-0003
BeanDeployment.recursiveBuild diamond defect. 9/9 PASS.

CLEAN markers: camel, hazelcast, tomcat, undertow, vertx — no diamond recursion pattern found.
Hazelcast uses proper Tarjan algorithm. Tomcat uses iterative constraint propagation.
This commit is contained in:
russell@unturf.com 2026-03-29 17:21:51 -04:00
parent 29308bfe00
commit 8a85da480d
8 changed files with 496 additions and 0 deletions

View file

@ -1,6 +1,7 @@
package unit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
@ -8,11 +9,13 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* Unit test for Quarkus CWE-407 defects:
* quarkus-0001: BeanInfo.getBoundInterceptors — bound.contains (ArrayList) in nested loops
* quarkus-0002: ComponentsProviderGenerator.isDependency — dependants.contains (ArrayList) in loop
* quarkus-0003: BeanDeployment.recursiveBuild — O(2^D) diamond recursion in transitive interceptor-binding resolution
*
* No JUnit. No external deps. Compile and run:
* javac -d . *.java && java -ea unit.QuarkusTest
@ -130,6 +133,107 @@ public class QuarkusTest {
return ops;
}
// ---- quarkus-0003 simulation ----
// Faithfully reproduces BeanDeployment.recursiveBuild() — no visited set.
//
// The defect: for each key in the map, recursiveBuild is called. Inside recursiveBuild,
// for every instance whose name is also a key, recursiveBuild is called AGAIN recursively.
// No visited set → diamond shapes cause O(2^D) calls.
//
// The map models: each node's Set<String> stores its direct children.
// recursiveBuild(name) expands the set to include ALL transitive children by mutation.
//
// Diamond structure built for depth D:
// nodes at each level 0..D-1 each have two children at the next level
// All nodes at level D-1 share a single leaf at level D.
// Example D=2: root->{b1,b2}, b1->{leaf}, b2->{leaf}
// Build annotation name set: node "n{id}" maps to its direct children.
// Creates a diamond graph where two branches merge at each level:
// root → {left_1, right_1}
// left_1 → {left_2, right_2}
// right_1 → {left_2, right_2}
// ...
// left_{D-1} → {leaf}
// right_{D-1} → {leaf}
// leaf → {}
// Every node visits its children; diamond convergence at every level causes
// exponential re-visitation without a visited set.
static Map<String, Set<String>> buildDiamond(int depth) {
Map<String, Set<String>> map = new HashMap<>();
String leaf = "leaf";
map.put(leaf, new HashSet<>());
// At each level, there is a "left" and "right" node (except the leaf).
// Both nodes at level L point to the same pair of nodes at level L+1.
String prevLeft = leaf, prevRight = null; // at leaf level only one node
for (int level = depth - 1; level >= 1; level--) {
String left = "L" + level + "_left";
String right = "L" + level + "_right";
Set<String> children;
if (prevRight == null) {
// previous level was single leaf; both new nodes point to leaf
children = new HashSet<>(Set.of(prevLeft));
} else {
children = new HashSet<>(Set.of(prevLeft, prevRight));
}
map.put(left, new HashSet<>(children));
map.put(right, new HashSet<>(children));
prevLeft = left;
prevRight = right;
}
// root points to both prevLeft and prevRight
Set<String> rootChildren = new HashSet<>();
rootChildren.add(prevLeft);
if (prevRight != null) rootChildren.add(prevRight);
map.put("root", rootChildren);
return map;
}
// Exact reproduction of the defect: recursiveBuild without visited set.
// Counts each invocation in callCount.
static Set<String> slowRecursiveBuild(String name,
Map<String, Set<String>> map,
AtomicLong callCount) {
callCount.incrementAndGet();
Set<String> result = map.get(name);
if (result == null) return Collections.emptySet();
// snapshot to avoid CME (defect code iterates transitiveBindingsMap.get(name) twice,
// we snapshot just as the defect's for-loop sees the set at entry time)
List<String> snapshot = new ArrayList<>(result);
for (String child : snapshot) {
if (map.containsKey(child)) {
result.addAll(slowRecursiveBuild(child, map, callCount)); // NO visited guard
}
}
return result;
}
// Fixed: recursiveBuild with visited set
static Set<String> fastRecursiveBuildWithVisited(String name,
Map<String, Set<String>> map,
Set<String> visited,
AtomicLong callCount) {
callCount.incrementAndGet();
if (!visited.add(name)) {
return map.getOrDefault(name, Collections.emptySet());
}
Set<String> result = map.get(name);
if (result == null) return Collections.emptySet();
for (String child : List.copyOf(result)) {
if (map.containsKey(child)) {
result.addAll(fastRecursiveBuildWithVisited(child, map, visited, callCount));
}
}
return result;
}
static Set<String> fastRecursiveBuildInner(String name, Map<String, Set<String>> map,
AtomicLong callCount, Set<String> visited) {
return fastRecursiveBuildWithVisited(name, map, visited, callCount);
}
public static void main(String[] args) {
int pass = 0;
int total = 0;
@ -228,6 +332,65 @@ public class QuarkusTest {
if (ok) pass++;
}
// --- quarkus-0003 tests ---
{
total++;
// Diamond depth=4: slow should make far more calls than fast
Map<String, Set<String>> map1 = buildDiamond(4);
AtomicLong slowCalls = new AtomicLong(0);
slowRecursiveBuild("root", map1, slowCalls);
Map<String, Set<String>> map2 = buildDiamond(4);
AtomicLong fastCalls = new AtomicLong(0);
fastRecursiveBuildWithVisited("root", map2, new HashSet<>(), fastCalls);
long sc = slowCalls.get(), fc = fastCalls.get();
boolean ok = sc > fc; // any measurable overhead; D=8 test validates exponential growth
System.out.println("[quarkus-0003] diamond D=4: slow_calls=" + sc + " fast_calls=" + fc +
" ratio=" + String.format("%.1f", (double) sc / Math.max(fc, 1)) + "x " + (ok ? "PASS" : "FAIL"));
if (ok) pass++;
}
{
total++;
// Diamond depth=8: exponential gap should be large
Map<String, Set<String>> map1 = buildDiamond(8);
AtomicLong slowCalls = new AtomicLong(0);
slowRecursiveBuild("root", map1, slowCalls);
Map<String, Set<String>> map2 = buildDiamond(8);
AtomicLong fastCalls = new AtomicLong(0);
fastRecursiveBuildWithVisited("root", map2, new HashSet<>(), fastCalls);
long sc = slowCalls.get(), fc = fastCalls.get();
boolean ok = sc > fc * 10;
System.out.println("[quarkus-0003] diamond D=8: slow_calls=" + sc + " fast_calls=" + fc +
" ratio=" + (sc / Math.max(fc, 1)) + "x " + (ok ? "PASS" : "FAIL"));
if (ok) pass++;
}
{
total++;
// Correctness: both approaches should collect the same transitive set
// Simple 3-node diamond: A->{B,C}, B->{D}, C->{D}, D->{}
Map<String, Set<String>> map3 = new HashMap<>();
map3.put("A", new HashSet<>(Set.of("B", "C")));
map3.put("B", new HashSet<>(Set.of("D")));
map3.put("C", new HashSet<>(Set.of("D")));
map3.put("D", new HashSet<>());
AtomicLong sc3 = new AtomicLong(0);
slowRecursiveBuild("A", map3, sc3);
Set<String> slowResult3 = map3.get("A"); // mutated in-place to include D
Map<String, Set<String>> map4 = new HashMap<>();
map4.put("A", new HashSet<>(Set.of("B", "C")));
map4.put("B", new HashSet<>(Set.of("D")));
map4.put("C", new HashSet<>(Set.of("D")));
map4.put("D", new HashSet<>());
AtomicLong fc3 = new AtomicLong(0);
Set<String> fastResult = fastRecursiveBuildWithVisited("A", map4, new HashSet<>(), fc3);
boolean ok = fastResult.containsAll(Set.of("B", "C", "D"))
&& slowResult3.containsAll(Set.of("B", "C", "D"));
System.out.println("[quarkus-0003] correctness: slow=" + slowResult3 +
" fast=" + fastResult + " " + (ok ? "PASS" : "FAIL"));
if (ok) pass++;
}
System.out.println("\n" + pass + "/" + total + " PASS");
if (pass != total) {
System.exit(1);