micronaut-core: 2 CWE-407 defects — uri-variable O(A×V) per request; topological-sort O(B²) stream scan + ArrayList prepend

This commit is contained in:
russell@unturf.com 2026-03-30 08:44:25 -04:00
parent b0a9a83efc
commit feda3896b9
3 changed files with 337 additions and 0 deletions

View file

@ -0,0 +1,37 @@
# UNDF: UNDF-2026-000000740
# UNDF: (leave blank)
--- a/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java
+++ b/http-client-core/src/main/java/io/micronaut/http/client/interceptor/HttpClientIntroductionAdvice.java
@@ -1,6 +1,7 @@
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
@@ -377,7 +377,7 @@
List<Argument<?>> bodyArguments = new ArrayList<>();
- List<String> uriVariables = uriTemplate.getVariableNames();
+ Set<String> uriVariables = new HashSet<>(uriTemplate.getVariableNames());
Map<String, MutableArgumentValue<?>> parameters = context.getParameters();
- ClientArgumentRequestBinder<Object> defaultBinder = buildDefaultBinder(pathParams, bodyArguments);
+ ClientArgumentRequestBinder<Object> defaultBinder = buildDefaultBinder(pathParams, bodyArguments, uriVariables);
// Apply all the method binders
@@ -404,7 +404,7 @@
- bindPathParams(uriVariables, pathParams, body);
+ bindPathParams(new ArrayList<>(uriVariables), pathParams, body);
@@ -499,8 +499,8 @@
- private ClientArgumentRequestBinder<Object> buildDefaultBinder(Map<String, Object> pathParams, List<Argument<?>> bodyArguments) {
+ private ClientArgumentRequestBinder<Object> buildDefaultBinder(Map<String, Object> pathParams, List<Argument<?>> bodyArguments, Set<String> uriVariableNames) {
return (ctx, uriCtx, value, req) -> {
Argument<?> argument = ctx.getArgument();
- if (uriCtx.getUriTemplate().getVariableNames().contains(argument.getName())) {
+ if (uriVariableNames.contains(argument.getName())) {
String name = argument.getAnnotationMetadata().stringValue(Bindable.class)
.orElse(argument.getName());

View file

@ -0,0 +1,81 @@
# UNDF: UNDF-2026-000000741
# UNDF: (leave blank)
--- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java
+++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java
@@ -3235,35 +3235,49 @@
private List<BeanRegistration> topologicalSort(Collection<BeanRegistration> beans) {
Map<Boolean, List<BeanRegistration>> initial = beans.stream()
.sorted(Comparator.comparing(s -> s.getBeanDefinition().getRequiredComponents().size()))
.collect(Collectors.groupingBy(b -> b.getBeanDefinition().getRequiredComponents().isEmpty()));
- List<BeanRegistration> sorted = new ArrayList<>(nullSafe(initial.get(true)));
+ // Use ArrayDeque for O(1) prepend (addFirst) instead of ArrayList O(B) add(0,x).
+ ArrayDeque<BeanRegistration> sortedDeque = new ArrayDeque<>(nullSafe(initial.get(true)));
List<BeanRegistration> unsorted = new ArrayList<>(nullSafe(initial.get(false)));
// Optimization which knows about types which are already in the sorted list
Set<Class<?>> satisfied = new HashSet<>();
// Optimization for types which we know are already unsatisified
// in a single iteration, allowing to skip the loop on unsorted elements
Set<Class<?>> unsatisfied = new HashSet<>();
+ // Pre-cache set of unsorted bean types to avoid O(B) stream reconstruction per component check.
+ // Updated when a bean is removed from unsorted.
+ Set<Class<?>> unsortedBeanTypes = new HashSet<>();
+ for (BeanRegistration br : unsorted) {
+ unsortedBeanTypes.add(br.getBeanDefinition().getBeanType());
+ }
+
//loop until all items have been sorted
while (!unsorted.isEmpty()) {
boolean acyclic = false;
unsatisfied.clear();
Iterator<BeanRegistration> i = unsorted.iterator();
while (i.hasNext()) {
BeanRegistration bean = i.next();
boolean found = false;
//determine if any components are in the unsorted list
Collection<Class<?>> components = bean.getBeanDefinition().getRequiredComponents();
for (Class<?> clazz : components) {
if (satisfied.contains(clazz)) {
continue;
}
- if (unsatisfied.contains(clazz) || unsorted.stream()
- .map(BeanRegistration::getBeanDefinition)
- .map(BeanDefinition::getBeanType)
- .anyMatch(clazz::isAssignableFrom)) {
+ if (unsatisfied.contains(clazz) || unsortedBeanTypes.stream()
+ .anyMatch(clazz::isAssignableFrom)) {
found = true;
unsatisfied.add(clazz);
break;
}
satisfied.add(clazz);
}
//none of the required components are in the unsorted list,
//so it can be added to the sorted list
if (!found) {
acyclic = true;
i.remove();
- sorted.add(0, bean);
+ sortedDeque.addFirst(bean);
+ unsortedBeanTypes.remove(bean.getBeanDefinition().getBeanType());
}
}
//rather than throw an exception here because there is a cyclical dependency
//just add the first item to the list and keep trying. It may be possible to
//see a cycle here because qualifiers are not taken into account.
if (!acyclic) {
- sorted.add(0, unsorted.remove(0));
+ BeanRegistration removed = unsorted.remove(0);
+ sortedDeque.addFirst(removed);
+ unsortedBeanTypes.remove(removed.getBeanDefinition().getBeanType());
}
}
- return sorted;
+ return new ArrayList<>(sortedDeque);
}

View file

@ -0,0 +1,219 @@
import java.util.*;
import java.util.stream.*;
/**
* CWE-407 unit tests for micronaut-core defects.
*
* micronaut-core-0001: HttpClientIntroductionAdvice.buildDefaultBinder
* calls uriCtx.getUriTemplate().getVariableNames().contains(argument.getName())
* inside a per-argument lambda. getVariableNames() allocates a new List<String>
* each invocation and List.contains() is O(V). Called once per argument per
* request O(A×V) per call.
* Fix: pre-compute Set<String> from variable names once before building the binder.
*
* micronaut-core-0002: DefaultBeanContext.topologicalSort
* Two defects:
* (a) unsorted.stream().map(...).map(...).anyMatch(clazz::isAssignableFrom) is
* O(B) stream reconstruction called for each component of each bean in each
* outer while-iteration O(B²×C) overall.
* Fix: pre-build Set<Class<?>> unsortedBeanTypes and update on removal.
* (b) sorted.add(0, bean) is O(B) ArrayList prepend O(B²) total.
* Fix: use ArrayDeque.addFirst() O(1).
*/
public class MicronautCoreTest {
// -----------------------------------------------------------------------
// micronaut-core-0001: URI variable lookup in HTTP client binder
// -----------------------------------------------------------------------
/** Simulates defective per-argument URI variable lookup using List.contains(). */
static boolean uriVarLookupDefect(List<String> argNames, List<String> uriVars) {
int hits = 0;
for (String arg : argNames) {
// Defect: getVariableNames() re-invoked inside lambda new List each time
List<String> freshList = new ArrayList<>(uriVars); // simulate getVariableNames()
if (freshList.contains(arg)) { // O(V) scan
hits++;
}
}
return hits > 0;
}
/** Fixed version: pre-compute Set<String> once, O(1) lookup. */
static boolean uriVarLookupFixed(List<String> argNames, List<String> uriVars) {
Set<String> varSet = new HashSet<>(uriVars); // computed once, O(V)
int hits = 0;
for (String arg : argNames) {
if (varSet.contains(arg)) { // O(1) per lookup
hits++;
}
}
return hits > 0;
}
static long timeUriVarLookup(boolean fixed, int argCount, int varCount) {
List<String> args = IntStream.range(0, argCount).mapToObj(i -> "arg" + i).collect(Collectors.toList());
List<String> vars = IntStream.range(0, varCount).mapToObj(i -> "var" + i).collect(Collectors.toList());
// Seed with one match so result is stable
if (!args.isEmpty() && !vars.isEmpty()) vars.set(0, args.get(0));
int REPS = 2000;
long start = System.nanoTime();
boolean r = false;
for (int rep = 0; rep < REPS; rep++) {
r = fixed ? uriVarLookupFixed(args, vars) : uriVarLookupDefect(args, vars);
}
long elapsed = System.nanoTime() - start;
if (!r) throw new AssertionError("should have matched");
return elapsed / REPS;
}
static void testUriVarLookup() {
int N = 100; // 100 args, 100 uri vars amplifies the O(A*V) cost
long defectNs = timeUriVarLookup(false, N, N);
long fixedNs = timeUriVarLookup(true, N, N);
double ratio = (double) defectNs / fixedNs;
System.out.printf("micronaut-core-0001 [N=%d]: defect=%dns fixed=%dns ratio=%.1fx%n",
N, defectNs, fixedNs, ratio);
if (ratio < 2.0) {
throw new AssertionError("Expected ratio >= 2.0x, got " + ratio);
}
System.out.println("micronaut-core-0001 PASS");
}
// -----------------------------------------------------------------------
// micronaut-core-0002: topologicalSort stream scan + ArrayList prepend
// -----------------------------------------------------------------------
/**
* Simulates the topologicalSort inner stream scan:
* for each bean, for each required component, scan all remaining unsorted beans.
* Defect: O(B²×C) stream rebuilt from List each check.
*/
static List<Integer> topoSortDefect(List<Integer[]> beans) {
// beans[i] = {beanId, requiredBeanId_or_-1_if_none}
List<Integer> unsorted = new ArrayList<>();
List<Integer> sorted = new ArrayList<>();
for (Integer[] b : beans) {
if (b[1] < 0) sorted.add(0, b[0]);
else unsorted.add(b[0]);
}
Map<Integer, Integer> required = new HashMap<>();
for (Integer[] b : beans) {
if (b[1] >= 0) required.put(b[0], b[1]);
}
while (!unsorted.isEmpty()) {
boolean acyclic = false;
Iterator<Integer> i = unsorted.iterator();
while (i.hasNext()) {
int bean = i.next();
int req = required.getOrDefault(bean, -1);
if (req < 0) {
i.remove();
sorted.add(0, bean); // O(B) prepend
acyclic = true;
} else {
// Defect: O(B) stream scan for each bean each iteration
boolean stillUnsorted = unsorted.stream().anyMatch(u -> u == req);
if (!stillUnsorted) {
i.remove();
sorted.add(0, bean); // O(B) prepend
acyclic = true;
}
}
}
if (!acyclic && !unsorted.isEmpty()) {
sorted.add(0, unsorted.remove(0));
}
}
return sorted;
}
/**
* Fixed version: pre-build Set<Integer> unsortedSet (O(1) lookup),
* use ArrayDeque for O(1) prepend.
*/
static List<Integer> topoSortFixed(List<Integer[]> beans) {
List<Integer> unsorted = new ArrayList<>();
ArrayDeque<Integer> sortedDeque = new ArrayDeque<>();
for (Integer[] b : beans) {
if (b[1] < 0) sortedDeque.addFirst(b[0]);
else unsorted.add(b[0]);
}
Map<Integer, Integer> required = new HashMap<>();
for (Integer[] b : beans) {
if (b[1] >= 0) required.put(b[0], b[1]);
}
// Pre-build set of unsorted bean IDs
Set<Integer> unsortedSet = new HashSet<>(unsorted);
while (!unsorted.isEmpty()) {
boolean acyclic = false;
Iterator<Integer> i = unsorted.iterator();
while (i.hasNext()) {
int bean = i.next();
int req = required.getOrDefault(bean, -1);
if (req < 0 || !unsortedSet.contains(req)) { // O(1)
i.remove();
sortedDeque.addFirst(bean); // O(1)
unsortedSet.remove(bean);
acyclic = true;
}
}
if (!acyclic && !unsorted.isEmpty()) {
int removed = unsorted.remove(0);
sortedDeque.addFirst(removed);
unsortedSet.remove(removed);
}
}
return new ArrayList<>(sortedDeque);
}
/** Build a chain: bean[i] requires bean[i-1], so they must sort in order. */
static List<Integer[]> buildBeanChain(int n) {
List<Integer[]> beans = new ArrayList<>();
beans.add(new Integer[]{0, -1}); // root, no dependency
for (int i = 1; i < n; i++) {
beans.add(new Integer[]{i, i - 1}); // bean i requires bean i-1
}
return beans;
}
static long timeTopoSort(boolean fixed, int n) {
List<Integer[]> beans = buildBeanChain(n);
int REPS = 100;
long start = System.nanoTime();
List<Integer> result = null;
for (int r = 0; r < REPS; r++) {
result = fixed ? topoSortFixed(new ArrayList<>(beans)) : topoSortDefect(new ArrayList<>(beans));
}
long elapsed = System.nanoTime() - start;
if (result == null || result.isEmpty()) throw new AssertionError("null result");
return elapsed / REPS;
}
static void testTopoSort() {
int N = 400; // chain of 400 beans O(B²) vs O(B) gap widens
long defectNs = timeTopoSort(false, N);
long fixedNs = timeTopoSort(true, N);
double ratio = (double) defectNs / fixedNs;
System.out.printf("micronaut-core-0002 [N=%d]: defect=%dns fixed=%dns ratio=%.1fx%n",
N, defectNs, fixedNs, ratio);
if (ratio < 2.0) {
throw new AssertionError("Expected ratio >= 2.0x, got " + ratio);
}
System.out.println("micronaut-core-0002 PASS");
}
// -----------------------------------------------------------------------
// main
// -----------------------------------------------------------------------
public static void main(String[] args) {
testUriVarLookup();
testTopoSort();
System.out.println("ALL PASS");
}
}