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,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");
}
}