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:
parent
b0a9a83efc
commit
feda3896b9
3 changed files with 337 additions and 0 deletions
|
|
@ -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());
|
||||
|
|
@ -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);
|
||||
}
|
||||
219
defects/micronaut-core/unit/MicronautCoreTest.java
Normal file
219
defects/micronaut-core/unit/MicronautCoreTest.java
Normal 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");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue