diff --git a/defects/hibernate/patch/hibernate-0007-fk-second-pass-diamond-recursion.patch b/defects/hibernate/patch/hibernate-0007-fk-second-pass-diamond-recursion.patch new file mode 100644 index 000000000..ba9001a73 --- /dev/null +++ b/defects/hibernate/patch/hibernate-0007-fk-second-pass-diamond-recursion.patch @@ -0,0 +1,38 @@ +--- a/hibernate-core/src/main/java/org/hibernate/boot/internal/InFlightMetadataCollectorImpl.java ++++ b/hibernate-core/src/main/java/org/hibernate/boot/internal/InFlightMetadataCollectorImpl.java +@@ -1803,7 +1803,8 @@ public class InFlightMetadataCollectorImpl implements InFlightMetadataCollector + // using the isADependencyOf map we order the FkSecondPass recursively instances into the right order for processing + final LinkedHashSet orderedFkSecondPassesSet = new LinkedHashSet<>(); + for ( String tableName : isADependencyOf.keySet() ) { +- buildRecursiveOrderedFkSecondPasses( orderedFkSecondPassesSet, isADependencyOf, tableName, tableName ); ++ buildRecursiveOrderedFkSecondPasses( orderedFkSecondPassesSet, isADependencyOf, tableName, tableName, ++ new HashSet<>() ); + } + // Reconstruct ordered list from set (order maintained by LinkedHashSet insertion sequence) + final List orderedFkSecondPasses = new ArrayList<>( orderedFkSecondPassesSet ); +@@ -1835,9 +1836,16 @@ public class InFlightMetadataCollectorImpl implements InFlightMetadataCollector + private void buildRecursiveOrderedFkSecondPasses( + LinkedHashSet orderedFkSecondPasses, + Map> isADependencyOf, + String startTable, +- String currentTable) { ++ String currentTable, ++ Set visitedTables) { ++ // hibernate-0007 fix: guard against diamond-graph exponential re-visitation. ++ // The original startTable identity check only prevented direct self-cycles; ++ // it did not prevent intermediate diamond nodes from being visited O(2^D) times. ++ // Adding visitedTables reduces the recursion from O(2^D) to O(T+E). ++ if ( !visitedTables.add( currentTable ) ) { ++ return; ++ } + final Set dependencies = isADependencyOf.get( currentTable ); + if ( dependencies != null ) { + for ( var fkSecondPass : dependencies ) { + final String dependentTable = fkSecondPass.getValue().getTable().getQualifiedTableName().render(); + if ( dependentTable.compareTo( startTable ) != 0 ) { +- buildRecursiveOrderedFkSecondPasses( orderedFkSecondPasses, isADependencyOf, startTable, dependentTable ); ++ buildRecursiveOrderedFkSecondPasses( orderedFkSecondPasses, isADependencyOf, startTable, ++ dependentTable, visitedTables ); + } + // O(1) with LinkedHashSet; was O(N) ArrayList.contains() + O(N) add(0,...) + orderedFkSecondPasses.add( fkSecondPass ); diff --git a/defects/hibernate/unit/HibernateFkDiamondRecursionTest.java b/defects/hibernate/unit/HibernateFkDiamondRecursionTest.java new file mode 100644 index 000000000..15fec1481 --- /dev/null +++ b/defects/hibernate/unit/HibernateFkDiamondRecursionTest.java @@ -0,0 +1,215 @@ +package unit; + +import java.util.*; + +/** + * Unit test for hibernate-0007: buildRecursiveOrderedFkSecondPasses diamond recursion O(2^D). + * + * The diamond structure (each level doubles paths back to a common ancestor): + * + * bottom + * / \ + * left_0 right_0 + * \ / + * mid_1 + * / \ + * left_1 right_1 + * \ / + * mid_2 + * ... + * top <-- startTable for outer loop iteration + * + * isADependencyOf["bottom"] = {(left_0 -> bottom), (right_0 -> bottom)} + * isADependencyOf["left_0"] = {(mid_1 -> left_0)} + * isADependencyOf["right_0"] = {(mid_1 -> right_0)} + * isADependencyOf["mid_1"] = {(left_1 -> mid_1), (right_1 -> mid_1)} + * ... + * + * "bottom" will be visited 2^D times by the buggy algorithm because there are 2^D + * paths from "top" down to "bottom" through the diamond chain. + */ +public class HibernateFkDiamondRecursionTest { + + // Pair: (fromTable, toTable) representing "fromTable has FK referencing toTable" + static class FK { + final String from; + final String to; + FK(String from, String to) { this.from = from; this.to = to; } + + @Override public String toString() { return from + "->" + to; } + + @Override public boolean equals(Object o) { + if (!(o instanceof FK)) return false; + FK f = (FK) o; + return from.equals(f.from) && to.equals(f.to); + } + + @Override public int hashCode() { return Objects.hash(from, to); } + } + + static int buggyCount; + static int fixedCount; + + // ---- BUGGY IMPLEMENTATION (no visitedTables) ---- + static void buildOrdered_BUGGY( + LinkedHashSet ordered, + Map> isADependencyOf, + String startTable, + String currentTable) { + buggyCount++; + Set deps = isADependencyOf.get(currentTable); + if (deps != null) { + for (FK fk : sorted(deps)) { + String dependentTable = fk.from; + if (!dependentTable.equals(startTable)) { + buildOrdered_BUGGY(ordered, isADependencyOf, startTable, dependentTable); + } + ordered.add(fk); + } + } + } + + // ---- FIXED IMPLEMENTATION (visitedTables prevents re-entry) ---- + static void buildOrdered_FIXED( + LinkedHashSet ordered, + Map> isADependencyOf, + String startTable, + String currentTable, + Set visitedTables) { + fixedCount++; + if (!visitedTables.add(currentTable)) { + return; // already visited this table — skip to avoid O(2^D) blowup + } + Set deps = isADependencyOf.get(currentTable); + if (deps != null) { + for (FK fk : sorted(deps)) { + String dependentTable = fk.from; + if (!dependentTable.equals(startTable)) { + buildOrdered_FIXED(ordered, isADependencyOf, startTable, dependentTable, visitedTables); + } + ordered.add(fk); + } + } + } + + static List sorted(Set fks) { + List list = new ArrayList<>(fks); + list.sort(Comparator.comparing((FK f) -> f.from).thenComparing(f -> f.to)); + return list; + } + + /** + * Build a diamond chain of depth D. + * Returns the map and the name of the "top" table (used as startTable). + */ + static Map> buildDiamondGraph(int depth) { + Map> map = new LinkedHashMap<>(); + String prev = "bottom"; + for (int i = 0; i < depth; i++) { + String left = "left_" + i; + String right = "right_" + i; + String top = (i < depth - 1) ? "mid_" + (i + 1) : "top"; + + // left and right both have FKs referencing prev + // -> isADependencyOf[prev] gets two entries + map.computeIfAbsent(prev, k -> new LinkedHashSet<>()).add(new FK(left, prev)); + map.computeIfAbsent(prev, k -> new LinkedHashSet<>()).add(new FK(right, prev)); + + // top has FKs referencing left and right + map.computeIfAbsent(left, k -> new LinkedHashSet<>()).add(new FK(top, left)); + map.computeIfAbsent(right, k -> new LinkedHashSet<>()).add(new FK(top, right)); + + prev = top; + } + return map; + } + + public static void main(String[] args) { + System.out.println("=== hibernate-0007: Diamond Recursion Test ===\n"); + + // Test 1: Measure exponential growth across depths + System.out.printf("%-8s %-12s %-12s %-10s%n", "Depth", "Buggy calls", "Fixed calls", "Ratio"); + for (int d = 1; d <= 13; d++) { + Map> map = buildDiamondGraph(d); + + buggyCount = 0; + LinkedHashSet buggyRes = new LinkedHashSet<>(); + for (String tbl : map.keySet()) { + buildOrdered_BUGGY(buggyRes, map, tbl, tbl); + } + int bCalls = buggyCount; + + fixedCount = 0; + LinkedHashSet fixedRes = new LinkedHashSet<>(); + for (String tbl : map.keySet()) { + buildOrdered_FIXED(fixedRes, map, tbl, tbl, new HashSet<>()); + } + int fCalls = fixedCount; + + double ratio = (fCalls > 0) ? (double) bCalls / fCalls : 0; + System.out.printf("%-8d %-12d %-12d %-10.1fx%n", d, bCalls, fCalls, ratio); + } + + // Test 2: At depth >= 10, buggy should be at least 20x more calls than fixed + { + Map> map = buildDiamondGraph(10); + + buggyCount = 0; + LinkedHashSet buggyRes = new LinkedHashSet<>(); + for (String tbl : map.keySet()) { + buildOrdered_BUGGY(buggyRes, map, tbl, tbl); + } + int bCalls = buggyCount; + + fixedCount = 0; + LinkedHashSet fixedRes = new LinkedHashSet<>(); + for (String tbl : map.keySet()) { + buildOrdered_FIXED(fixedRes, map, tbl, tbl, new HashSet<>()); + } + int fCalls = fixedCount; + + double ratio = (double) bCalls / fCalls; + System.out.printf("%nDepth-10: buggy=%d, fixed=%d, ratio=%.1fx%n", bCalls, fCalls, ratio); + + assert ratio >= 20.0 : "Expected >=20x blowup at depth=10, got " + ratio; + System.out.println("ASSERT PASS: buggy/fixed ratio >= 20x at depth=10"); + } + + // Test 3: Correctness — both produce identical FK sets + for (int d = 1; d <= 6; d++) { + Map> map = buildDiamondGraph(d); + + buggyCount = 0; + LinkedHashSet buggyRes = new LinkedHashSet<>(); + for (String tbl : map.keySet()) { + buildOrdered_BUGGY(buggyRes, map, tbl, tbl); + } + + fixedCount = 0; + LinkedHashSet fixedRes = new LinkedHashSet<>(); + for (String tbl : map.keySet()) { + buildOrdered_FIXED(fixedRes, map, tbl, tbl, new HashSet<>()); + } + + assert buggyRes.equals(fixedRes) : + "depth=" + d + ": buggy=" + buggyRes + " fixed=" + fixedRes; + } + System.out.println("ASSERT PASS: buggy and fixed produce identical FK sets at depths 1-6"); + + // Test 4: Fixed should complete depth=20 quickly; buggy would need ~2M calls + { + Map> map = buildDiamondGraph(20); + fixedCount = 0; + LinkedHashSet fixedRes = new LinkedHashSet<>(); + for (String tbl : map.keySet()) { + buildOrdered_FIXED(fixedRes, map, tbl, tbl, new HashSet<>()); + } + int fCalls = fixedCount; + System.out.printf("%nDepth-20 fixed calls: %d (should be small)%n", fCalls); + assert fCalls < 100_000 : "Fixed should be O(N) at depth=20: " + fCalls; + System.out.println("ASSERT PASS: fixed is O(N) even at depth=20"); + } + + System.out.println("\n=== ALL ASSERTIONS PASSED ==="); + } +} diff --git a/defects/spring/patch/spring-diamond-recursion-CLEAN.md b/defects/spring/patch/spring-diamond-recursion-CLEAN.md new file mode 100644 index 000000000..d7affef80 --- /dev/null +++ b/defects/spring/patch/spring-diamond-recursion-CLEAN.md @@ -0,0 +1,25 @@ +# Spring Framework — Diamond Recursion (CWE-407 O(2^D)) Scan: CLEAN + +**Pattern:** Recursive cycle-detection / dependency traversal without a visited set +(exponential re-visitation on diamond-shaped DAGs) +**Scan date:** 2026-03-29 +**Scope:** `spring-beans`, `spring-context`, `spring-core`, `spring-aop`, `spring-web` + +## Methods Checked + +| Method | Location | Guard | Result | +|--------|----------|-------|--------| +| `DefaultSingletonBeanRegistry.isDependent` | `spring-beans/.../support/DefaultSingletonBeanRegistry.java:630` | `Set alreadySeen` parameter — O(1) HashSet | CLEAN | +| `DefaultLifecycleProcessor.doStart` | `spring-context/.../support/DefaultLifecycleProcessor.java:395` | `lifecycleBeans.remove(beanName)` — once removed, bean skipped on re-entry | CLEAN | +| `DefaultLifecycleProcessor.doStop` | `spring-context/.../support/DefaultLifecycleProcessor.java:458` | Same `lifecycleBeans.remove` guard | CLEAN | +| `DefaultListableBeanFactory.hasPrimaryConflict` | `spring-beans/.../support/DefaultListableBeanFactory.java:2272` | Traverses parent bean factory chain (linear, not DAG) | CLEAN | +| `DefaultListableBeanFactory.checkBeanNotOfRequiredType` | `spring-beans/.../support/DefaultListableBeanFactory.java:2304` | Traverses parent chain (linear) | CLEAN | +| `ConfigurationClassParser.collectImports` | `spring-context/.../annotation/ConfigurationClassParser.java:562` | `Set visited` parameter | CLEAN | +| `AnnotationTypeMapping.computeSynthesizableFlag` | `spring-core/.../annotation/AnnotationTypeMapping.java:261` | `Set> visitedAnnotationTypes` passed through | CLEAN | + +## Conclusion + +No diamond recursion defect found in Spring Framework. All recursive graph-traversal +methods have proper visited-set guards. The `isDependent` method (the most critical +path for circular bean dependency detection) was fixed in a prior version to add +`alreadySeen` — the fix predates this scan.