diff --git a/defects/micronaut-core/patch/micronaut-core-0001-http-client-uri-variable-lookup.patch b/defects/micronaut-core/patch/micronaut-core-0001-http-client-uri-variable-lookup.patch new file mode 100644 index 000000000..59462ca24 --- /dev/null +++ b/defects/micronaut-core/patch/micronaut-core-0001-http-client-uri-variable-lookup.patch @@ -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> bodyArguments = new ArrayList<>(); + +- List uriVariables = uriTemplate.getVariableNames(); ++ Set uriVariables = new HashSet<>(uriTemplate.getVariableNames()); + Map> parameters = context.getParameters(); + +- ClientArgumentRequestBinder defaultBinder = buildDefaultBinder(pathParams, bodyArguments); ++ ClientArgumentRequestBinder 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 buildDefaultBinder(Map pathParams, List> bodyArguments) { ++ private ClientArgumentRequestBinder buildDefaultBinder(Map pathParams, List> bodyArguments, Set 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()); diff --git a/defects/micronaut-core/patch/micronaut-core-0002-topological-sort-stream-scan.patch b/defects/micronaut-core/patch/micronaut-core-0002-topological-sort-stream-scan.patch new file mode 100644 index 000000000..c2cdb45f8 --- /dev/null +++ b/defects/micronaut-core/patch/micronaut-core-0002-topological-sort-stream-scan.patch @@ -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 topologicalSort(Collection beans) { + Map> initial = beans.stream() + .sorted(Comparator.comparing(s -> s.getBeanDefinition().getRequiredComponents().size())) + .collect(Collectors.groupingBy(b -> b.getBeanDefinition().getRequiredComponents().isEmpty())); +- List sorted = new ArrayList<>(nullSafe(initial.get(true))); ++ // Use ArrayDeque for O(1) prepend (addFirst) instead of ArrayList O(B) add(0,x). ++ ArrayDeque sortedDeque = new ArrayDeque<>(nullSafe(initial.get(true))); + List unsorted = new ArrayList<>(nullSafe(initial.get(false))); + // Optimization which knows about types which are already in the sorted list + Set> 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> 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> 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 i = unsorted.iterator(); + while (i.hasNext()) { + BeanRegistration bean = i.next(); + boolean found = false; + + //determine if any components are in the unsorted list + Collection> 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); + } diff --git a/defects/micronaut-core/unit/MicronautCoreTest.java b/defects/micronaut-core/unit/MicronautCoreTest.java new file mode 100644 index 000000000..58932b270 --- /dev/null +++ b/defects/micronaut-core/unit/MicronautCoreTest.java @@ -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 + * each invocation and List.contains() is O(V). Called once per argument per + * request → O(A×V) per call. + * Fix: pre-compute Set 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> 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 argNames, List uriVars) { + int hits = 0; + for (String arg : argNames) { + // Defect: getVariableNames() re-invoked inside lambda → new List each time + List freshList = new ArrayList<>(uriVars); // simulate getVariableNames() + if (freshList.contains(arg)) { // O(V) scan + hits++; + } + } + return hits > 0; + } + + /** Fixed version: pre-compute Set once, O(1) lookup. */ + static boolean uriVarLookupFixed(List argNames, List uriVars) { + Set 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 args = IntStream.range(0, argCount).mapToObj(i -> "arg" + i).collect(Collectors.toList()); + List 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 topoSortDefect(List beans) { + // beans[i] = {beanId, requiredBeanId_or_-1_if_none} + List unsorted = new ArrayList<>(); + List sorted = new ArrayList<>(); + for (Integer[] b : beans) { + if (b[1] < 0) sorted.add(0, b[0]); + else unsorted.add(b[0]); + } + Map required = new HashMap<>(); + for (Integer[] b : beans) { + if (b[1] >= 0) required.put(b[0], b[1]); + } + + while (!unsorted.isEmpty()) { + boolean acyclic = false; + Iterator 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 unsortedSet (O(1) lookup), + * use ArrayDeque for O(1) prepend. + */ + static List topoSortFixed(List beans) { + List unsorted = new ArrayList<>(); + ArrayDeque sortedDeque = new ArrayDeque<>(); + for (Integer[] b : beans) { + if (b[1] < 0) sortedDeque.addFirst(b[0]); + else unsorted.add(b[0]); + } + Map 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 unsortedSet = new HashSet<>(unsorted); + + while (!unsorted.isEmpty()) { + boolean acyclic = false; + Iterator 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 buildBeanChain(int n) { + List 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 beans = buildBeanChain(n); + int REPS = 100; + long start = System.nanoTime(); + List 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"); + } +}