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.
396 lines
17 KiB
Java
396 lines
17 KiB
Java
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
|
|
*/
|
|
public class MicronautTest {
|
|
|
|
// ---- micronaut-0001 simulation ----
|
|
// Simulates resolveHierarchy: builds hierarchy list while walking superclass chain,
|
|
// calling contains() to deduplicate. Also simulates populateHierarchyInterfaces recursion.
|
|
|
|
static long slowResolveHierarchy(int superclassCount, int interfacesPerClass) {
|
|
long ops = 0;
|
|
List<Integer> hierarchy = new ArrayList<>();
|
|
List<Integer> interfaces = new ArrayList<>();
|
|
|
|
// Walk superclass chain
|
|
for (int superclass = 0; superclass < superclassCount; superclass++) {
|
|
if (!hierarchy.contains(superclass)) { // O(H) ArrayList
|
|
ops += hierarchy.size() + 1;
|
|
hierarchy.add(superclass);
|
|
}
|
|
// Populate interfaces for this superclass
|
|
for (int iface = 0; iface < interfacesPerClass; iface++) {
|
|
int ifaceId = superclass * 100 + iface;
|
|
if (!interfaces.contains(ifaceId)) { // O(|interfaces|) ArrayList
|
|
ops += interfaces.size() + 1;
|
|
interfaces.add(ifaceId);
|
|
}
|
|
// Recursive: each interface may have parent interfaces
|
|
for (int parentIface = 0; parentIface < interfacesPerClass / 2; parentIface++) {
|
|
int parentId = ifaceId * 100 + parentIface;
|
|
if (!interfaces.contains(parentId)) { // O(|interfaces|)
|
|
ops += interfaces.size() + 1;
|
|
interfaces.add(parentId);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return ops;
|
|
}
|
|
|
|
static long fastResolveHierarchy(int superclassCount, int interfacesPerClass) {
|
|
long ops = 0;
|
|
Set<Integer> hierarchy = new LinkedHashSet<>();
|
|
Set<Integer> interfaces = new LinkedHashSet<>();
|
|
|
|
for (int superclass = 0; superclass < superclassCount; superclass++) {
|
|
if (!hierarchy.contains(superclass)) { // O(1) HashSet
|
|
ops += 1;
|
|
hierarchy.add(superclass);
|
|
}
|
|
for (int iface = 0; iface < interfacesPerClass; iface++) {
|
|
int ifaceId = superclass * 100 + iface;
|
|
if (!interfaces.contains(ifaceId)) { // O(1)
|
|
ops += 1;
|
|
interfaces.add(ifaceId);
|
|
}
|
|
for (int parentIface = 0; parentIface < interfacesPerClass / 2; parentIface++) {
|
|
int parentId = ifaceId * 100 + parentIface;
|
|
if (!interfaces.contains(parentId)) { // O(1)
|
|
ops += 1;
|
|
interfaces.add(parentId);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return ops;
|
|
}
|
|
|
|
// ---- micronaut-0002 simulation ----
|
|
// Simulates addRepeatableStereotype: for each parent in parents list,
|
|
// check annotationList.contains (ArrayList) before adding.
|
|
|
|
static long slowAddRepeatableStereotype(int parentCount, int existingAnnotations) {
|
|
long ops = 0;
|
|
List<String> annotationList = new ArrayList<>();
|
|
// Pre-populate with existingAnnotations
|
|
for (int i = 0; i < existingAnnotations; i++) {
|
|
annotationList.add("existing-" + i);
|
|
}
|
|
// Add parents
|
|
for (int i = 0; i < parentCount; i++) {
|
|
String parent = "parent-" + i;
|
|
if (!annotationList.contains(parent)) { // O(|annotationList|) ArrayList
|
|
ops += annotationList.size() + 1;
|
|
annotationList.add(parent);
|
|
}
|
|
}
|
|
return ops;
|
|
}
|
|
|
|
static long fastAddRepeatableStereotype(int parentCount, int existingAnnotations) {
|
|
long ops = 0;
|
|
Set<String> annotationSet = new LinkedHashSet<>();
|
|
for (int i = 0; i < existingAnnotations; i++) {
|
|
annotationSet.add("existing-" + i);
|
|
}
|
|
for (int i = 0; i < parentCount; i++) {
|
|
String parent = "parent-" + i;
|
|
if (!annotationSet.contains(parent)) { // O(1)
|
|
ops += 1;
|
|
annotationSet.add(parent);
|
|
}
|
|
}
|
|
return ops;
|
|
}
|
|
|
|
// ---- micronaut-0003 simulation ----
|
|
// Simulates getEnv: for each env var, check excludes.contains and includes.contains
|
|
|
|
static long slowEnvFilter(int envVarCount, int filterListSize) {
|
|
long ops = 0;
|
|
List<String> excludes = new ArrayList<>();
|
|
List<String> includes = new ArrayList<>();
|
|
for (int i = 0; i < filterListSize; i++) {
|
|
excludes.add("EXCLUDE_" + i);
|
|
includes.add("INCLUDE_" + i);
|
|
}
|
|
Map<String, String> env = new HashMap<>();
|
|
for (int i = 0; i < envVarCount; i++) {
|
|
env.put("ENV_VAR_" + i, "value");
|
|
}
|
|
for (String envVar : env.keySet()) {
|
|
if (excludes.contains(envVar)) { // O(filterListSize) ArrayList
|
|
ops += filterListSize;
|
|
continue;
|
|
}
|
|
if (!includes.contains(envVar)) { // O(filterListSize) ArrayList
|
|
ops += filterListSize;
|
|
continue;
|
|
}
|
|
ops += filterListSize * 2;
|
|
}
|
|
return ops;
|
|
}
|
|
|
|
static long fastEnvFilter(int envVarCount, int filterListSize) {
|
|
long ops = 0;
|
|
Set<String> excludes = new HashSet<>();
|
|
Set<String> includes = new HashSet<>();
|
|
for (int i = 0; i < filterListSize; i++) {
|
|
excludes.add("EXCLUDE_" + i);
|
|
includes.add("INCLUDE_" + i);
|
|
}
|
|
Map<String, String> env = new HashMap<>();
|
|
for (int i = 0; i < envVarCount; i++) {
|
|
env.put("ENV_VAR_" + i, "value");
|
|
}
|
|
for (String envVar : env.keySet()) {
|
|
if (excludes.contains(envVar)) { // O(1)
|
|
ops += 1;
|
|
continue;
|
|
}
|
|
if (!includes.contains(envVar)) { // O(1)
|
|
ops += 1;
|
|
continue;
|
|
}
|
|
ops += 2;
|
|
}
|
|
return ops;
|
|
}
|
|
|
|
public static void main(String[] args) {
|
|
int pass = 0;
|
|
int total = 0;
|
|
|
|
// --- micronaut-0001 tests ---
|
|
{
|
|
total++;
|
|
long slow = slowResolveHierarchy(20, 5);
|
|
long fast = fastResolveHierarchy(20, 5);
|
|
boolean ok = slow > fast * 5;
|
|
System.out.println("[micronaut-0001] C=20 I=5: slow_ops=" + slow + " fast_ops=" + fast +
|
|
" ratio=" + (slow / Math.max(fast, 1)) + "x " + (ok ? "PASS" : "FAIL"));
|
|
if (ok) pass++;
|
|
}
|
|
{
|
|
total++;
|
|
// Correctness: same unique elements discovered
|
|
List<Integer> slowHierarchy = new ArrayList<>();
|
|
Set<Integer> fastHierarchy = new LinkedHashSet<>();
|
|
for (int i = 0; i < 30; i++) {
|
|
int val = i % 15;
|
|
if (!slowHierarchy.contains(val)) slowHierarchy.add(val);
|
|
fastHierarchy.add(val);
|
|
}
|
|
boolean ok = slowHierarchy.size() == fastHierarchy.size();
|
|
System.out.println("[micronaut-0001] correctness: slow=" + slowHierarchy.size() +
|
|
" fast=" + fastHierarchy.size() + " " + (ok ? "PASS" : "FAIL"));
|
|
if (ok) pass++;
|
|
}
|
|
|
|
// --- micronaut-0002 tests ---
|
|
{
|
|
total++;
|
|
long slow = slowAddRepeatableStereotype(30, 10);
|
|
long fast = fastAddRepeatableStereotype(30, 10);
|
|
boolean ok = slow > fast * 3;
|
|
System.out.println("[micronaut-0002] P=30 existing=10: slow_ops=" + slow + " fast_ops=" + fast +
|
|
" ratio=" + (slow / Math.max(fast, 1)) + "x " + (ok ? "PASS" : "FAIL"));
|
|
if (ok) pass++;
|
|
}
|
|
{
|
|
total++;
|
|
long slow = slowAddRepeatableStereotype(100, 50);
|
|
long fast = fastAddRepeatableStereotype(100, 50);
|
|
boolean ok = slow > fast * 10;
|
|
System.out.println("[micronaut-0002] P=100 existing=50: slow_ops=" + slow + " fast_ops=" + fast +
|
|
" ratio=" + (slow / Math.max(fast, 1)) + "x " + (ok ? "PASS" : "FAIL"));
|
|
if (ok) pass++;
|
|
}
|
|
{
|
|
total++;
|
|
// Correctness: same dedup result
|
|
List<String> slowResult = new ArrayList<>();
|
|
Set<String> fastResult = new LinkedHashSet<>();
|
|
String[] parents = {"a", "b", "a", "c", "b", "d"};
|
|
for (String p : parents) {
|
|
if (!slowResult.contains(p)) slowResult.add(p);
|
|
fastResult.add(p);
|
|
}
|
|
boolean ok = slowResult.size() == fastResult.size() &&
|
|
new ArrayList<>(fastResult).equals(slowResult);
|
|
System.out.println("[micronaut-0002] dedup correctness: slow=" + slowResult.size() +
|
|
" fast=" + fastResult.size() + " " + (ok ? "PASS" : "FAIL"));
|
|
if (ok) pass++;
|
|
}
|
|
|
|
// --- micronaut-0003 tests ---
|
|
{
|
|
total++;
|
|
long slow = slowEnvFilter(500, 50);
|
|
long fast = fastEnvFilter(500, 50);
|
|
boolean ok = slow > fast * 10;
|
|
System.out.println("[micronaut-0003] E=500 N=50: slow_ops=" + slow + " fast_ops=" + fast +
|
|
" ratio=" + (slow / Math.max(fast, 1)) + "x " + (ok ? "PASS" : "FAIL"));
|
|
if (ok) pass++;
|
|
}
|
|
{
|
|
total++;
|
|
// Correctness: same env vars pass through filter
|
|
List<String> slowPassed = new ArrayList<>();
|
|
List<String> fastPassed = new ArrayList<>();
|
|
List<String> excludeList = new ArrayList<>();
|
|
Set<String> excludeSet = new HashSet<>();
|
|
List<String> includeList = new ArrayList<>();
|
|
Set<String> includeSet = new HashSet<>();
|
|
for (int i = 0; i < 5; i++) {
|
|
excludeList.add("EX_" + i); excludeSet.add("EX_" + i);
|
|
includeList.add("ENV_VAR_" + i); includeSet.add("ENV_VAR_" + i);
|
|
}
|
|
for (int i = 0; i < 10; i++) {
|
|
String v = "ENV_VAR_" + i;
|
|
if (!excludeList.contains(v) && includeList.contains(v)) slowPassed.add(v);
|
|
if (!excludeSet.contains(v) && includeSet.contains(v)) fastPassed.add(v);
|
|
}
|
|
boolean ok = slowPassed.size() == fastPassed.size();
|
|
System.out.println("[micronaut-0003] filter correctness: slow=" + slowPassed.size() +
|
|
" fast=" + fastPassed.size() + " " + (ok ? "PASS" : "FAIL"));
|
|
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);
|
|
}
|
|
}
|
|
}
|