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.
399 lines
17 KiB
Java
399 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.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
|
||
*/
|
||
public class QuarkusTest {
|
||
|
||
// ---- quarkus-0001 simulation ----
|
||
// Simulates getBoundInterceptors(): nested loops over lifecycle + intercepted methods,
|
||
// deduplicating into 'bound' list using ArrayList.contains
|
||
|
||
static long slowGetBoundInterceptors(int methodCount, int interceptorsPerMethod) {
|
||
long ops = 0;
|
||
List<Integer> bound = new ArrayList<>();
|
||
|
||
// Loop 1: lifecycleInterceptors.values()
|
||
for (int m = 0; m < methodCount / 2; m++) {
|
||
for (int i = 0; i < interceptorsPerMethod; i++) {
|
||
int interceptorId = i; // interceptors reused across methods (dedup needed)
|
||
ops += bound.size() + 1; // cost of ArrayList.contains scan
|
||
if (!bound.contains(interceptorId)) {
|
||
bound.add(interceptorId);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Loop 2: interceptedMethods.values()
|
||
for (int m = methodCount / 2; m < methodCount; m++) {
|
||
for (int i = 0; i < interceptorsPerMethod; i++) {
|
||
int interceptorId = i;
|
||
ops += bound.size() + 1;
|
||
if (!bound.contains(interceptorId)) {
|
||
bound.add(interceptorId);
|
||
}
|
||
}
|
||
}
|
||
|
||
return ops;
|
||
}
|
||
|
||
static long fastGetBoundInterceptors(int methodCount, int interceptorsPerMethod) {
|
||
long ops = 0;
|
||
Set<Integer> boundSet = new LinkedHashSet<>();
|
||
|
||
for (int m = 0; m < methodCount / 2; m++) {
|
||
for (int i = 0; i < interceptorsPerMethod; i++) {
|
||
int interceptorId = i;
|
||
ops += 1; // O(1) HashSet.contains
|
||
boundSet.add(interceptorId);
|
||
}
|
||
}
|
||
|
||
for (int m = methodCount / 2; m < methodCount; m++) {
|
||
for (int i = 0; i < interceptorsPerMethod; i++) {
|
||
int interceptorId = i;
|
||
ops += 1;
|
||
boundSet.add(interceptorId);
|
||
}
|
||
}
|
||
|
||
// Convert to sorted List at end (one-time O(I log I))
|
||
List<Integer> bound = new ArrayList<>(boundSet);
|
||
return ops;
|
||
}
|
||
|
||
// ---- quarkus-0002 simulation ----
|
||
// Simulates isDependency called O(B) times, each iterating map values (O(B)) and
|
||
// calling dependants.contains (ArrayList, O(D)).
|
||
|
||
static long slowIsDependency(int beanCount, int dependantsPerBean) {
|
||
long ops = 0;
|
||
// dependencyMap: bean → list of dependants
|
||
Map<Integer, List<Integer>> dependencyMap = new TreeMap<>();
|
||
for (int b = 0; b < beanCount; b++) {
|
||
List<Integer> dependants = new ArrayList<>();
|
||
for (int d = 0; d < dependantsPerBean; d++) {
|
||
dependants.add((b + d + 1) % beanCount);
|
||
}
|
||
dependencyMap.put(b, dependants);
|
||
}
|
||
|
||
// isDependency called for each bean (O(B) calls total)
|
||
for (int queryBean = 0; queryBean < beanCount; queryBean++) {
|
||
for (List<Integer> dependants : dependencyMap.values()) { // O(B) map values
|
||
ops += dependants.size(); // ArrayList.contains scan cost
|
||
if (dependants.contains(queryBean)) {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
return ops;
|
||
}
|
||
|
||
static long fastIsDependency(int beanCount, int dependantsPerBean) {
|
||
long ops = 0;
|
||
Map<Integer, List<Integer>> dependencyMap = new TreeMap<>();
|
||
for (int b = 0; b < beanCount; b++) {
|
||
List<Integer> dependants = new ArrayList<>();
|
||
for (int d = 0; d < dependantsPerBean; d++) {
|
||
dependants.add((b + d + 1) % beanCount);
|
||
}
|
||
dependencyMap.put(b, dependants);
|
||
}
|
||
|
||
// Build inverted index once: O(B×D)
|
||
Set<Integer> allDependants = new HashSet<>();
|
||
for (List<Integer> dependants : dependencyMap.values()) {
|
||
allDependants.addAll(dependants);
|
||
}
|
||
|
||
// isDependency is now O(1) per call
|
||
for (int queryBean = 0; queryBean < beanCount; queryBean++) {
|
||
ops += 1; // O(1) HashSet.contains
|
||
allDependants.contains(queryBean);
|
||
}
|
||
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;
|
||
|
||
// --- quarkus-0001 tests ---
|
||
{
|
||
total++;
|
||
long slow = slowGetBoundInterceptors(20, 8);
|
||
long fast = fastGetBoundInterceptors(20, 8);
|
||
boolean ok = slow > fast * 3;
|
||
System.out.println("[quarkus-0001] M=20 I=8: slow_ops=" + slow + " fast_ops=" + fast +
|
||
" ratio=" + (slow / Math.max(fast, 1)) + "x " + (ok ? "PASS" : "FAIL"));
|
||
if (ok) pass++;
|
||
}
|
||
{
|
||
total++;
|
||
long slow = slowGetBoundInterceptors(50, 15);
|
||
long fast = fastGetBoundInterceptors(50, 15);
|
||
boolean ok = slow > fast * 5;
|
||
System.out.println("[quarkus-0001] M=50 I=15: slow_ops=" + slow + " fast_ops=" + fast +
|
||
" ratio=" + (slow / Math.max(fast, 1)) + "x " + (ok ? "PASS" : "FAIL"));
|
||
if (ok) pass++;
|
||
}
|
||
{
|
||
total++;
|
||
// Correctness: both paths must produce same unique interceptor count
|
||
Set<Integer> slowBound = new LinkedHashSet<>();
|
||
Set<Integer> fastBound = new LinkedHashSet<>();
|
||
int methods = 10, interceptors = 5;
|
||
// slow: uses ArrayList dedup but we track the same set for checking
|
||
List<Integer> slowList = new ArrayList<>();
|
||
for (int m = 0; m < methods; m++) {
|
||
for (int i = 0; i < interceptors; i++) {
|
||
if (!slowList.contains(i)) slowList.add(i);
|
||
}
|
||
}
|
||
Set<Integer> fastSet = new LinkedHashSet<>();
|
||
for (int m = 0; m < methods; m++) {
|
||
for (int i = 0; i < interceptors; i++) {
|
||
fastSet.add(i);
|
||
}
|
||
}
|
||
boolean ok = slowList.size() == fastSet.size();
|
||
System.out.println("[quarkus-0001] correctness: slow=" + slowList.size() +
|
||
" fast=" + fastSet.size() + " " + (ok ? "PASS" : "FAIL"));
|
||
if (ok) pass++;
|
||
}
|
||
|
||
// --- quarkus-0002 tests ---
|
||
{
|
||
total++;
|
||
long slow = slowIsDependency(100, 5);
|
||
long fast = fastIsDependency(100, 5);
|
||
boolean ok = slow > fast * 20;
|
||
System.out.println("[quarkus-0002] B=100 D=5: slow_ops=" + slow + " fast_ops=" + fast +
|
||
" ratio=" + (slow / Math.max(fast, 1)) + "x " + (ok ? "PASS" : "FAIL"));
|
||
if (ok) pass++;
|
||
}
|
||
{
|
||
total++;
|
||
long slow = slowIsDependency(300, 10);
|
||
long fast = fastIsDependency(300, 10);
|
||
boolean ok = slow > fast * 100;
|
||
System.out.println("[quarkus-0002] B=300 D=10: slow_ops=" + slow + " fast_ops=" + fast +
|
||
" ratio=" + (slow / Math.max(fast, 1)) + "x " + (ok ? "PASS" : "FAIL"));
|
||
if (ok) pass++;
|
||
}
|
||
{
|
||
total++;
|
||
// Correctness: isDependency returns same true/false for same query
|
||
Map<Integer, List<Integer>> dmap = new HashMap<>();
|
||
dmap.put(0, new ArrayList<>(List.of(1, 2, 3)));
|
||
dmap.put(1, new ArrayList<>(List.of(4, 5)));
|
||
dmap.put(2, new ArrayList<>(List.of(6)));
|
||
|
||
// slow: iterate all lists, call contains
|
||
boolean slowResult3 = false;
|
||
boolean slowResult7 = false;
|
||
for (List<Integer> deps : dmap.values()) {
|
||
if (deps.contains(3)) { slowResult3 = true; break; }
|
||
}
|
||
for (List<Integer> deps : dmap.values()) {
|
||
if (deps.contains(7)) { slowResult7 = true; break; }
|
||
}
|
||
|
||
// fast: precompute set
|
||
Set<Integer> allDeps = new HashSet<>();
|
||
for (List<Integer> deps : dmap.values()) allDeps.addAll(deps);
|
||
boolean fastResult3 = allDeps.contains(3);
|
||
boolean fastResult7 = allDeps.contains(7);
|
||
|
||
boolean ok = slowResult3 == fastResult3 && slowResult7 == fastResult7
|
||
&& slowResult3 == true && slowResult7 == false;
|
||
System.out.println("[quarkus-0002] isDependency correctness: bean3=" + fastResult3 +
|
||
" bean7=" + fastResult7 + " " + (ok ? "PASS" : "FAIL"));
|
||
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);
|
||
}
|
||
}
|
||
}
|