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,18 +1,21 @@
package unit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
/**
* Unit test for Micronaut CWE-407 defects:
* micronaut-0001: ClassUtils.resolveHierarchy hierarchy.contains (ArrayList) in while loop
* micronaut-0002: MutableAnnotationMetadata annotationList.contains (ArrayList) in for loop
* micronaut-0003: EnvironmentPropertySource includes/excludes.contains (List) in env loop
* micronaut-0004: AbstractAnnotationMetadataBuilder.processAnnotation O(2^D) diamond recursion in meta-annotation traversal
*
* No JUnit. No external deps. Compile and run:
* javac -d . *.java && java -ea unit.MicronautTest
@ -275,9 +278,119 @@ public class MicronautTest {
if (ok) pass++;
}
// --- micronaut-0004 tests ---
// Simulates AbstractAnnotationMetadataBuilder.processAnnotation diamond recursion.
// The guard context.isProcessed() only checks CURRENT PATH ancestors not globally
// visited nodes so a diamond meta-annotation graph causes O(2^D) re-visits.
//
// Model: annotation name set of meta-annotations (stereotypes)
// processAnnotation(ctx, name): check if name in ctx.parentAnnotations; if not,
// recursively process each stereotype with ctx.withParent(name)
{
total++;
// Diamond depth=8: slow ~383 calls, fast ~29 calls, ratio ~13x
Map<String, Set<String>> stereotypes = buildMetaDiamond(8);
AtomicLong slowCalls = new AtomicLong(0);
slowProcessAnnotation("root", stereotypes, new HashSet<>(), slowCalls);
Map<String, Set<String>> stereotypes2 = buildMetaDiamond(8);
AtomicLong fastCalls = new AtomicLong(0);
fastProcessAnnotation("root", stereotypes2, new HashSet<>(), new HashSet<>(), fastCalls);
long sc = slowCalls.get(), fc = fastCalls.get();
boolean ok = sc > fc * 5;
System.out.println("[micronaut-0004] diamond D=8: slow=" + sc +
" fast=" + fc + " ratio=" + (sc/Math.max(fc,1)) + "x " + (ok ? "PASS" : "FAIL"));
if (ok) pass++;
}
{
total++;
// Diamond depth=10: slow ~1535 calls, fast ~37 calls, ratio ~41x
Map<String, Set<String>> stereotypes = buildMetaDiamond(10);
AtomicLong slowCalls = new AtomicLong(0);
slowProcessAnnotation("root", stereotypes, new HashSet<>(), slowCalls);
Map<String, Set<String>> stereotypes2 = buildMetaDiamond(10);
AtomicLong fastCalls = new AtomicLong(0);
fastProcessAnnotation("root", stereotypes2, new HashSet<>(), new HashSet<>(), fastCalls);
long sc = slowCalls.get(), fc = fastCalls.get();
boolean ok = sc > fc * 20;
System.out.println("[micronaut-0004] diamond D=10: slow=" + sc +
" fast=" + fc + " ratio=" + (sc/Math.max(fc,1)) + "x " + (ok ? "PASS" : "FAIL"));
if (ok) pass++;
}
{
total++;
// Correctness: fast traversal visits all nodes in diamond
Map<String, Set<String>> s2 = buildMetaDiamond(5);
Set<String> fastCollected = new HashSet<>();
AtomicLong fc = new AtomicLong(0);
fastProcessAnnotation("root", s2, new HashSet<>(), fastCollected, fc);
// All nodes should be visited: root, L1..L4 left/right pairs, leaf
boolean ok = fastCollected.contains("root") && fastCollected.contains("leaf")
&& fastCollected.size() == s2.size();
System.out.println("[micronaut-0004] correctness: visited=" + fastCollected.size() +
"/" + s2.size() + " nodes " + (ok ? "PASS" : "FAIL"));
if (ok) pass++;
}
System.out.println("\n" + pass + "/" + total + " PASS");
if (pass != total) {
System.exit(1);
}
}
// ---- micronaut-0004 helpers ----
// Build diamond meta-annotation graph of depth D.
// root {left_1, right_1}; left_i {left_{i+1}, right_{i+1}}; right_i {left_{i+1}, right_{i+1}}; leaf {}
static Map<String, Set<String>> buildMetaDiamond(int depth) {
Map<String, Set<String>> map = new HashMap<>();
String leaf = "leaf";
map.put(leaf, new HashSet<>());
String prevLeft = leaf, prevRight = null;
for (int level = depth - 1; level >= 1; level--) {
String left = "L" + level + "_left";
String right = "L" + level + "_right";
Set<String> children = new HashSet<>();
children.add(prevLeft);
if (prevRight != null) children.add(prevRight);
map.put(left, new HashSet<>(children));
map.put(right, new HashSet<>(children));
prevLeft = left;
prevRight = right;
}
Set<String> rootChildren = new HashSet<>();
rootChildren.add(prevLeft);
if (prevRight != null) rootChildren.add(prevRight);
map.put("root", rootChildren);
return map;
}
// Defect simulation: processAnnotation without global visited set.
// parentAnnotations = current-path ancestors only (isProcessed guard per the defect).
static void slowProcessAnnotation(String name, Map<String, Set<String>> stereotypesMap,
Set<String> parentAnnotations, AtomicLong callCount) {
callCount.incrementAndGet();
if (parentAnnotations.contains(name)) return; // cycle guard (current path only)
Set<String> stereotypes = stereotypesMap.getOrDefault(name, Collections.emptySet());
// withParent: add name to parent set for child calls
Set<String> newParents = new HashSet<>(parentAnnotations);
newParents.add(name);
for (String stereotype : new ArrayList<>(stereotypes)) {
slowProcessAnnotation(stereotype, stereotypesMap, newParents, callCount); // NO global visited
}
}
// Fixed: processAnnotation with global visited set.
static void fastProcessAnnotation(String name, Map<String, Set<String>> stereotypesMap,
Set<String> parentAnnotations, Set<String> globalVisited,
AtomicLong callCount) {
callCount.incrementAndGet();
if (parentAnnotations.contains(name)) return; // cycle guard
if (!globalVisited.add(name)) return; // global visited guard (FIX)
Set<String> stereotypes = stereotypesMap.getOrDefault(name, Collections.emptySet());
Set<String> newParents = new HashSet<>(parentAnnotations);
newParents.add(name);
for (String stereotype : new ArrayList<>(stereotypes)) {
fastProcessAnnotation(stereotype, stereotypesMap, newParents, globalVisited, callCount);
}
}
}