java-topology/defects/quarkus/patch/quarkus-0001-beaninfo-bound-interceptors-contains.md

3.3 KiB
Raw Permalink Blame History

UNDF: UNDF-2026-000000517

quarkus-0001: BeanInfo.getBoundInterceptors — O(I²) bound.contains in nested loops

CWE-407 — Algorithmic Complexity: Linear Membership Test in Loop

Field Value
ID quarkus-0001
Severity HIGH
Ecosystem quarkus
Package arc/processor
File independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java
Lines 493, 500, 522
Complexity O(I²) where I = total interceptors across all interception registrations
Fix Use LinkedHashSet<InterceptorInfo> for bound collection

Description

getBoundInterceptors() builds a deduplicated List<InterceptorInfo> bound = new ArrayList<>(). For each InterceptionInfo in lifecycleInterceptors.values() (outer loop), it iterates interception.interceptors (inner loop) and calls bound.contains(interceptor) — O(|bound|) on ArrayList. The same pattern repeats for interceptedMethods.values().

Similarly, getBoundDecorators() at line 522 has the same pattern with List<DecoratorInfo>.

List<InterceptorInfo> bound = new ArrayList<>();
for (InterceptionInfo interception : lifecycleInterceptors.values()) {  // O(M)
    for (InterceptorInfo interceptor : interception.interceptors) {     // O(I_per_method)
        if (!bound.contains(interceptor)) {                             // O(|bound|) ArrayList
            bound.add(interceptor);
        }
    }
}
for (InterceptionInfo interception : interceptedMethods.values()) {     // O(M)
    for (InterceptorInfo interceptor : interception.interceptors) {     // O(I_per_method)
        if (!bound.contains(interceptor)) {                             // O(|bound|) ArrayList
            bound.add(interceptor);
        }
    }
}

Compounding Effect

getBoundInterceptors() is not cached — it recomputes on every call. It is called from:

  • ComponentsProviderGenerator.initBeanDependencyMap() — in a loop over all beans
  • BeanDeployment — in loops checking removable interceptors (lines 429, 434)
  • BeanGenerator, SubclassGenerator, InterceptionProxyGenerator — multiple hot paths

This means the O(I²) computation is repeated multiple times per bean per build phase.

Fix

// Before
List<InterceptorInfo> bound = new ArrayList<>();
// dedup via bound.contains()

// After — use LinkedHashSet for O(1) contains, convert to sorted List at end
Set<InterceptorInfo> boundSet = new LinkedHashSet<>();
for (InterceptionInfo interception : lifecycleInterceptors.values()) {
    for (InterceptorInfo interceptor : interception.interceptors) {
        boundSet.add(interceptor);  // Set.add handles dedup, O(1)
    }
}
for (InterceptionInfo interception : interceptedMethods.values()) {
    for (InterceptorInfo interceptor : interception.interceptors) {
        boundSet.add(interceptor);
    }
}
List<InterceptorInfo> bound = new ArrayList<>(boundSet);
Collections.sort(bound);

Same pattern for getBoundDecorators().

Additionally, consider caching the result (memoize after initialize() is called).

Speedup Estimate

For a bean with 10 intercepted methods × 5 interceptors each: 50 × 25 average bound size = 1,250 operations → 50. 25x speedup per call, multiplied by the number of call sites per build.