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
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