diff --git a/defects/activemq-artemis/patch/artemis-0002-fileconfigparser-allroles-list-contains.md b/defects/activemq-artemis/patch/artemis-0002-fileconfigparser-allroles-list-contains.md new file mode 100644 index 000000000..e90dfc270 --- /dev/null +++ b/defects/activemq-artemis/patch/artemis-0002-fileconfigparser-allroles-list-contains.md @@ -0,0 +1,101 @@ +# UNDF: UNDF-2026-000000234 +# UNDF: (pending) +# artemis-0002: FileConfigurationParser.parseSecurityRoles — O(N×R) multiple ArrayList.contains in role loop + +## CWE-407 — Algorithmic Complexity: Unnecessary Quadratic Complexity (List membership inside loop) + +| Field | Value | +|-------|-------| +| ID | artemis-0002 | +| Severity | MEDIUM | +| Ecosystem | activemq-artemis | +| Package | org.apache.activemq.artemis.core.deployers.impl | +| File | `artemis-server/src/main/java/org/apache/activemq/artemis/core/deployers/impl/FileConfigurationParser.java` | +| Lines | 1212–1275 | +| Complexity | O(N×R) | +| Hot path | broker startup / configuration reload | + +## Defect + +`FileConfigurationParser` parses broker security roles from XML configuration. +It builds per-permission `ArrayList` collections (send, consume, +createDurableQueue, deleteDurableQueue, createNonDurableQueue, deleteNonDurableQueue, +manageRoles, browseRoles, createAddressRoles, deleteAddressRoles, viewRoles, +editRoles), then for each role in `allRoles` (N roles) it calls `.contains(role)` +on up to 12 of these lists. Each call is O(R) where R is the maximum number of +roles in any single permission list. Total cost: O(N × 12 × R) = O(N²) in the +degenerate case where all roles have all permissions. + +Additionally, `allRoles` itself is populated using `!allRoles.contains(role.trim())` +which is O(N) per insertion — another O(N²) scan. + +```java +// FileConfigurationParser.java lines 1212-1275 +List send = new ArrayList<>(); +List consume = new ArrayList<>(); +// ... 10 more ArrayList ... +List allRoles = new ArrayList<>(); + +// ... XML parsing loop populates the lists ... +if (!allRoles.contains(role.trim())) { // O(N) per role — O(N²) total + allRoles.add(role.trim()); +} + +for (String role : allRoles) { + securityRoles.add(new Role(role, + send.contains(role), // O(R) × 12 per role + consume.contains(role), + createDurableQueue.contains(role), + deleteDurableQueue.contains(role), + createNonDurableQueue.contains(role), + deleteNonDurableQueue.contains(role), + manageRoles.contains(role), + browseRoles.contains(role), + createAddressRoles.contains(role), + deleteAddressRoles.contains(role), + viewRoles.contains(role), + editRoles.contains(role))); +} +``` + +## Fix + +Use `HashSet` (or `LinkedHashSet` to preserve order) for all the +permission lists, and for `allRoles`: + +```java +Set send = new LinkedHashSet<>(); +Set consume = new LinkedHashSet<>(); +Set createDurableQueue = new LinkedHashSet<>(); +Set deleteDurableQueue = new LinkedHashSet<>(); +Set createNonDurableQueue = new LinkedHashSet<>(); +Set deleteNonDurableQueue = new LinkedHashSet<>(); +Set manageRoles = new LinkedHashSet<>(); +Set browseRoles = new LinkedHashSet<>(); +Set createAddressRoles = new LinkedHashSet<>(); +Set deleteAddressRoles = new LinkedHashSet<>(); +Set viewRoles = new LinkedHashSet<>(); +Set editRoles = new LinkedHashSet<>(); +Set allRoles = new LinkedHashSet<>(); + +// allRoles population: add() already deduplicates in a Set — no contains() check needed +allRoles.add(role.trim()); // replaces the !allRoles.contains() guard + +// Role construction loop unchanged — Set.contains() is O(1) +for (String role : allRoles) { + securityRoles.add(new Role(role, + send.contains(role), consume.contains(role), ...)); +} +``` + +## Speedup + +| N (roles) | R (roles per permission) | Before (ops) | After (ops) | Speedup | +|-----------|--------------------------|-------------|-------------|---------| +| 50 | 50 | ~30,000 | ~600 | 50× | +| 200 | 200 | ~480,000 | ~2,400 | 200× | +| 1,000 | 1,000 | ~12,000,000 | ~12,000 | 1,000× | + +Most Artemis deployments have modest role counts (10–50) but enterprise LDAP-backed +configurations with fine-grained per-address permissions can have hundreds of roles. +The defect is hit on every broker startup and every live configuration reload. diff --git a/defects/box2d/patch/box2d-0001-broad-phase-index-map.patch b/defects/box2d/patch/box2d-0001-broad-phase-index-map.patch index a7a5ef46c..45ce9d627 100644 --- a/defects/box2d/patch/box2d-0001-broad-phase-index-map.patch +++ b/defects/box2d/patch/box2d-0001-broad-phase-index-map.patch @@ -1,4 +1,4 @@ -# UNDF: UNDF-2026-000000015 +# UNDF: UNDF-2026-000000235 --- a/src/broad_phase.h +++ b/src/broad_phase.h @@ -30,8 +30,9 @@ typedef struct b2BroadPhase diff --git a/defects/doris/patch/doris-0002-plannode-conjuncts-linkedhashset.md b/defects/doris/patch/doris-0002-plannode-conjuncts-linkedhashset.md index 1faac6ac0..244db0364 100644 --- a/defects/doris/patch/doris-0002-plannode-conjuncts-linkedhashset.md +++ b/defects/doris/patch/doris-0002-plannode-conjuncts-linkedhashset.md @@ -1,3 +1,4 @@ +# UNDF: UNDF-2026-000000289 # UNDF: (pending) # doris-0002: PlanNode.addConjunct — ArrayList.contains() O(C²) dedup diff --git a/defects/dry/patch/dry-0001-listview-hashset.patch b/defects/dry/patch/dry-0001-listview-hashset.patch index 51801d56d..32b20fe05 100644 --- a/defects/dry/patch/dry-0001-listview-hashset.patch +++ b/defects/dry/patch/dry-0001-listview-hashset.patch @@ -1,4 +1,4 @@ -# UNDF: UNDF-2026-000000054 +# UNDF: UNDF-2026-000000314 --- a/Source/Dry/UI/ListView.h +++ b/Source/Dry/UI/ListView.h @@ -... ListView class members diff --git a/defects/dry/patch/dry-0002-object-unsub-hashset.patch b/defects/dry/patch/dry-0002-object-unsub-hashset.patch index fbdeb7a4d..806094b57 100644 --- a/defects/dry/patch/dry-0002-object-unsub-hashset.patch +++ b/defects/dry/patch/dry-0002-object-unsub-hashset.patch @@ -1,4 +1,4 @@ -# UNDF: UNDF-2026-000000055 +# UNDF: UNDF-2026-000000316 --- a/Source/Dry/Core/Object.cpp +++ b/Source/Dry/Core/Object.cpp @@ -269,12 +269,16 @@ void Object::UnsubscribeFromAllEventsExcept(const PODVector& exceptions, bool onlyUserData) diff --git a/defects/grpc/patch/CLEAN.md b/defects/grpc/patch/CLEAN.md index 812f339bc..e6e1234b9 100644 --- a/defects/grpc/patch/CLEAN.md +++ b/defects/grpc/patch/CLEAN.md @@ -1,17 +1,16 @@ -# CLEAN — gRPC (multi-language) -Scanned 2026-03-29 for CWE-407. +# CLEAN — gRPC +Scanned 2026-03-29 for CWE-407 (algorithmic complexity). ## Scope - -- Java source: `examples/android/helloworld/` only (no Java RPC core in this clone) -- C++ core: `core/lib/`, `src/core/` — service resolution, call handling +The cloned `~/git/grpc` repository is the gRPC C/C++ core repo, not the Java +implementation (`grpc-java`). No Java source files were found in `src/` beyond +Android example activities. ## Findings +- No Java main-path source files present in this clone. +- gRPC Java (`io.grpc:grpc-*`) ships as a separate Maven/Gradle repository + (`grpc/grpc-java` on GitHub) which was not cloned. -gRPC's Java implementation in this repository is limited to example code. The core RPC runtime is implemented in C++ and uses protobuf-generated descriptors with flat-array or hash-map lookups for service and method resolution. No quadratic list-membership pattern was found in: - -- Method descriptor building (protobuf ServiceDescriptor uses array indexing) -- Service type resolution (registry uses hash map) -- Call filter/interceptor chains (fixed-size arrays assembled at startup) - -**Result: No actionable CWE-407 defects.** +## Result +**CLEAN (N/A). No Java CWE-407 defects findable in this clone — wrong repo flavor.** +**Action: clone `grpc/grpc-java` for a proper Java scan if needed.** diff --git a/defects/guice/patch/CLEAN.md b/defects/guice/patch/CLEAN.md new file mode 100644 index 000000000..d5c6d7474 --- /dev/null +++ b/defects/guice/patch/CLEAN.md @@ -0,0 +1,27 @@ +# CLEAN — Google Guice +Scanned 2026-03-29 for CWE-407 (algorithmic complexity). + +## Scope +Module scanned: core/src (binding processor, injection point resolution, type listener store, +provision listener store, cycle-detecting lock, annotations). + +## Findings + +### MembersInjectorStore — alreadySeenListeners.contains() +`alreadySeenListeners` is declared `Set alreadySeenListeners = Sets.newHashSet()`. +O(1) membership checks. Clean. + +### BindingSourceRestriction — currentModulePermits.contains() +`currentModulePermits` is `ImmutableSet>`. O(1). Clean. + +### AbstractBindingProcessor — FORBIDDEN_TYPES.contains() +`FORBIDDEN_TYPES` is an `ImmutableSet`. O(1). Clean. + +### ProvisionListenerCallbackStore — INTERNAL_BINDINGS.contains() +`INTERNAL_BINDINGS` is an `ImmutableSet>`. O(1). Clean. + +### InjectorJitBindingData — failedJitBindings.contains(), bannedKeys.contains() +Both fields are `Set` implementations. Clean. + +## Result +**CLEAN. No actionable CWE-407 defects found in Google Guice.** diff --git a/defects/hibernate/patch/CLEAN.md b/defects/hibernate/patch/CLEAN.md new file mode 100644 index 000000000..294c286d2 --- /dev/null +++ b/defects/hibernate/patch/CLEAN.md @@ -0,0 +1,30 @@ +# CLEAN — Hibernate ORM +Scanned 2026-03-29 for CWE-407 (algorithmic complexity). + +## Scope +hibernate-core/src/main/java — SqmUtil, ActionQueue, BulkOperationCleanupAction, +LoadQueryInfluencers, StatefulPersistenceContext, ToOneAttributeMapping, +ManyToManyCollectionPart, BaseSqmToSqlAstConverter, GeneratedValuesProcessor. + +## Findings + +### ActionQueue — tableSpaces.contains() +Parameter `Set tableSpaces` — O(1). Clean. + +### BulkOperationCleanupAction — affectedTableSpaces.contains() +Parameter is `Set` — O(1). Clean. + +### LoadQueryInfluencers — enabledFetchProfileNames.contains() +`enabledFetchProfileNames` is `HashSet` — O(1). Clean. + +### ToOneAttributeMapping — targetKeyPropertyNames.contains() +`targetKeyPropertyNames` is `Set` backed by `HashSet` — O(1). Clean. + +### StatefulPersistenceContext — nullAssociations.contains(), insertedEntityIds.contains() +Both backed by `HashSet` implementations — O(1). Clean. + +### BaseSqmToSqlAstConverter — visitedAssociationKeys.contains(), excludedEntityNames.contains() +Both are `Set` or `HashSet` fields — O(1). Clean. + +## Result +**CLEAN. No actionable CWE-407 defects found in Hibernate ORM.** diff --git a/defects/kotlin/patch/kotlin-0001-nonexpansive-collectreachable-hashset.md b/defects/kotlin/patch/kotlin-0001-nonexpansive-collectreachable-hashset.md new file mode 100644 index 000000000..96fb06e5d --- /dev/null +++ b/defects/kotlin/patch/kotlin-0001-nonexpansive-collectreachable-hashset.md @@ -0,0 +1,105 @@ +# UNDF: UNDF-2026-000000134 +# UNDF: (pending) +# kotlin-0001: NonExpansiveInheritanceRestrictionChecker.collectReachable — O(E×V) list scan on reachability result + +## CWE-407 — Algorithmic Complexity: Inefficient Algorithmic Complexity + +| Field | Value | +|-------|-------| +| ID | kotlin-0001 | +| Severity | HIGH | +| Ecosystem | kotlin | +| Package | org.jetbrains.kotlin.resolve | +| File | `compiler/frontend/src/org/jetbrains/kotlin/resolve/NonExpansiveInheritanceRestrictionChecker.kt` | +| Lines | 150–169 | +| Complexity | O(E × V) — was O(V) list scan per edge in cycle check | +| Hot path | Type-checking of generic class declarations with complex supertype bounds | + +## Defect + +`collectReachable` builds the set of type-parameter nodes reachable from a given start node +during the expansive-inheritance check (SLS §4.5 / KLS §11.2). The original implementation +returns a `List` via `DFS.NodeHandlerWithListResult`. The result is immediately consumed +in `isEdgeInCycle`: + +```kotlin +private fun Graph.isEdgeInCycle(edge: ExpansiveEdge) = + edge.from in collectReachable(edge.to) + +private fun Graph.collectReachable(from: T): List { // BUG: List + val handler = object : DFS.NodeHandlerWithListResult() { + override fun afterChildren(current: T?) { + result.add(current) + } + } + val neighbors = object : DFS.Neighbors { + override fun getNeighbors(current: T): Iterable = + this@collectReachable.getNeighbors(current) + } + DFS.dfs(listOf(from), neighbors, handler) + return handler.result() +} +``` + +`edge.from in collectReachable(edge.to)` uses Kotlin's `in` operator on a `List`, which +compiles to `List.contains()` — a linear O(V) scan. The check is performed once per +expansive edge, so with E expansive edges and V type-parameter nodes the total cost is +**O(E × V)**. For a deeply layered generic hierarchy (e.g. a sealed-trait diamond with +many type parameters), E and V grow together, making this O(V²) in the worst case. + +## Fix + +Accumulate the reachable nodes into a `HashSet` directly and return `Set`, so the +`in` membership test at the call site becomes O(1): + +```kotlin +private fun Graph.isEdgeInCycle(edge: ExpansiveEdge) = + edge.from in collectReachable(edge.to) // unchanged — but now O(1) + +// Return Set so `in` is O(1) hash lookup instead of O(V) list scan +private fun Graph.collectReachable(from: T): Set { + val reachable = hashSetOf() + + val handler = object : DFS.NodeHandlerWithListResult() { + override fun afterChildren(current: T?) { + if (current != null) reachable.add(current) + } + } + + val neighbors = object : DFS.Neighbors { + override fun getNeighbors(current: T): Iterable = + this@collectReachable.getNeighbors(current) + } + + DFS.dfs(listOf(from), neighbors, handler) + + return reachable +} +``` + +`TypeParameterDescriptor` already implements `equals`/`hashCode` (identity by object), +so storing it in a `HashSet` is correct and safe. + +## Speedup + +| V (type params) | E (expansive edges) | Before (ops) | After (ops) | Speedup | +|-----------------|---------------------|-------------|-------------|---------| +| 10 | 10 | 100 | 10 | 10× | +| 50 | 50 | 2,500 | 50 | 50× | +| 100 | 100 | 10,000 | 100 | 100× | +| 200 | 200 | 40,000 | 200 | 200× | + +Real-world Kotlin classes rarely exceed V=20 type parameters, but annotation-heavy +codebases (frameworks using typeclasses / phantom types) can push higher. The patch +removes an entire complexity class regardless of input size. + +## Affected Versions + +All Kotlin compiler versions that include `NonExpansiveInheritanceRestrictionChecker` +(introduced circa Kotlin 1.0; still present in Kotlin 2.x frontend compatibility layer). + +## References + +- JetBrains YouTrack (none filed) +- Upstream source: `compiler/frontend/src/org/jetbrains/kotlin/resolve/NonExpansiveInheritanceRestrictionChecker.kt` +- CWE-407: https://cwe.mitre.org/data/definitions/407.html diff --git a/defects/kotlin/patch/kotlin-0002-constraint-system-bounds-linkedhashset.md b/defects/kotlin/patch/kotlin-0002-constraint-system-bounds-linkedhashset.md new file mode 100644 index 000000000..7dad1508d --- /dev/null +++ b/defects/kotlin/patch/kotlin-0002-constraint-system-bounds-linkedhashset.md @@ -0,0 +1,92 @@ +# UNDF: UNDF-2026-000000135 +# UNDF: (pending) +# kotlin-0002: ConstraintSystemBuilderImpl.addBound — O(B) ArrayList scan per constraint during type inference + +## CWE-407 — Algorithmic Complexity: Inefficient Algorithmic Complexity + +| Field | Value | +|-------|-------| +| ID | kotlin-0002 | +| Severity | MEDIUM | +| Ecosystem | kotlin | +| Package | org.jetbrains.kotlin.resolve.calls.inference | +| File | `compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/inference/ConstraintSystemBuilderImpl.kt` | +| Lines | 266–279 | +| Complexity | O(B) duplicate-check per addBound call; O(B²) total per type variable during constraint incorporation | +| Hot path | Type inference for every generic call site | + +## Defect + +`ConstraintSystemBuilderImpl.addBound()` deduplicates bounds before adding them to prevent +the constraint incorporation loop from diverging. The dedup check uses `ArrayList.contains()`: + +```kotlin +// TypeBoundsImpl.kt:31 +override val bounds = ArrayList() // BUG: ArrayList + +// ConstraintSystemBuilderImpl.kt:277 +val typeBounds = getTypeBounds(typeVariable) +if (typeBounds.bounds.contains(bound)) return // O(B) linear scan per call +typeBounds.addBound(bound) +``` + +`TypeBounds.Bound` defines `equals()` and `hashCode()` (comparing `typeVariable`, +`constrainingType`, `kind`, and `position.isStrong()`), making it fully eligible for +storage in a hash-based collection. + +During constraint incorporation (`constraintIncorporation.kt`), `incorporateBound()` is +called once per new bound and may call `addBound` O(B) times as it cross-multiplies with +existing bounds. Each `addBound` performs an O(B) `contains` scan, yielding **O(B²) +total** duplicate-check work per type variable per incorporation round. For functions with +many type parameters and complex bounds (e.g. higher-kinded types, SAM conversions with +many overloads), B can reach 30–50 during a single call resolution. + +## Fix + +Change `TypeBoundsImpl.bounds` from `ArrayList` to `LinkedHashSet` to +provide O(1) `contains` and maintain insertion order for deterministic value computation: + +```kotlin +// TypeBoundsImpl.kt — change bounds to LinkedHashSet +class TypeBoundsImpl(override val typeVariable: TypeVariable) : TypeBounds { + override val bounds: MutableSet = LinkedHashSet() // was: ArrayList() + + fun addBound(bound: Bound) { + resultValues = null + assert(bound.typeVariable == typeVariable) { ... } + bounds.add(bound) + } + // ... rest unchanged +} +``` + +The `TypeBounds` interface declares `bounds: Collection`, so changing the concrete +type to `LinkedHashSet` is a compatible implementation change. All iteration patterns +(`bounds.indices`, `bounds.filter`, `bounds.any`, `bounds.flatMap`) work identically on +`Set` as on `List`. + +The dedup guard in `ConstraintSystemBuilderImpl.addBound()` can then rely on +`LinkedHashSet.add()` returning `false` on duplicate, or retain the explicit check — both +become O(1). + +## Speedup + +| B (bounds per type var) | Before (contains ops) | After (contains ops) | Speedup | +|-------------------------|-----------------------|----------------------|---------| +| 10 | 100 | 10 | 10× | +| 30 | 900 | 30 | 30× | +| 50 | 2,500 | 50 | 50× | + +Typical Kotlin inference sessions for moderate generics see B ≈ 5–15; complex higher-kinded +scenarios can reach B = 30–50. The patch provides consistent O(1) dedup across all cases. + +## Affected Versions + +All Kotlin compiler versions with `ConstraintSystemBuilderImpl` / `TypeBoundsImpl` +(Kotlin 1.0+; still present in K1 frontend used by kotlinc and IntelliJ IDEA K1 mode). + +## References + +- JetBrains YouTrack (none filed) +- Upstream source: `compiler/frontend/src/org/jetbrains/kotlin/resolve/calls/inference/` +- CWE-407: https://cwe.mitre.org/data/definitions/407.html diff --git a/defects/netty/patch/CLEAN.md b/defects/netty/patch/CLEAN.md new file mode 100644 index 000000000..c6160499a --- /dev/null +++ b/defects/netty/patch/CLEAN.md @@ -0,0 +1,29 @@ +# CLEAN — Netty +Scanned 2026-03-29 for CWE-407 (algorithmic complexity). + +## Scope +Modules scanned: common, handler (SSL/TLS), codec-http, codec-http2, transport. + +## Findings + +### handler/ssl/JdkBaseApplicationProtocolNegotiator — NoFailProtocolSelector.select() +`for (String p : supportedProtocols)` iterates a `Set` (field declared as +`Set supportedProtocols`) and calls `protocols.contains(p)` on a `List`. +The outer set is O(S) where S = supported ALPN protocol count (typically 1–3); inner +list scan is O(P) where P = negotiated protocol count (also typically 1–3). Total O(S×P) +but bounded to small constants — not actionable. + +### handler/ssl/JdkBaseApplicationProtocolNegotiator — NoFailProtocolSelectionListener.selected() +`supportedProtocols.contains(protocol)` — single call, not in a loop. Not a CWE-407 issue. + +### handler/ssl/SslUtils.addIfSupported() +`supported.contains(n)` — `supported` is declared `Set`. O(1). Clean. + +### codec-http2/HttpConversionUtil — HTTP_TO_HTTP2_HEADER_BLACKLIST.contains() +Both `HTTP_TO_HTTP2_HEADER_BLACKLIST` and `connectionBlacklist` are `Set` — O(1). Clean. + +### transport/NioChannelOption — supportedOptions().contains() +Returns `Set>` from the JDK. Clean. + +## Result +**CLEAN. No actionable CWE-407 defects found in Netty.** diff --git a/defects/nifi/patch/nifi-0001-parameter-context-cycle-stack-hashset.md b/defects/nifi/patch/nifi-0001-parameter-context-cycle-stack-hashset.md new file mode 100644 index 000000000..d8b67f28b --- /dev/null +++ b/defects/nifi/patch/nifi-0001-parameter-context-cycle-stack-hashset.md @@ -0,0 +1,76 @@ +# UNDF: UNDF-2026-000000472 +# UNDF: (pending) +# nifi-0001: StandardParameterContext.verifyNoCycles — O(D²) Stack.contains on DFS path + +## CWE-407 — Algorithmic Complexity + +| Field | Value | +|-------|-------| +| ID | nifi-0001 | +| Severity | MEDIUM | +| Ecosystem | nifi | +| Package | nifi-framework-components | +| File | `nifi-framework-bundle/nifi-framework/nifi-framework-components/src/main/java/org/apache/nifi/parameter/StandardParameterContext.java` | +| Lines | 532–556 | +| Complexity | O(D²) | +| Hot path | Called on every ParameterContext inheritance update | + +## Defect + +`verifyNoCycles` uses a `Stack` (which extends `Vector`) to track the DFS +path and calls `traversedIds.contains(id)` to detect back-edges. `Stack.contains()` is an +O(D) linear scan, called once per node at each recursion level, giving O(D²) total where D is +the depth of the ParameterContext inheritance chain. + +```java +private void verifyNoCycles(final Stack traversedIds, + final List parameterContexts) { + for (final ParameterContext parameterContext : parameterContexts) { + final String id = parameterContext.getIdentifier(); + if (traversedIds.contains(id)) { // O(D) linear scan + throw new IllegalStateException(...); + } + traversedIds.push(id); + verifyNoCycles(traversedIds, parameterContext.getInheritedParameterContexts()); + traversedIds.pop(); + } +} +``` + +## Fix + +Replace `Stack` with a `HashSet` for the visited-in-current-path set. Since +`Stack` is used as a DFS path tracker, we need O(1) membership checks. Use a `Set` for +the cycle check and a separate `Deque` only if ordering is needed (it is not here). + +```java +private void verifyNoCycles(final List parameterContexts) { + final Set traversedIds = new HashSet<>(); + traversedIds.add(id); + verifyNoCycles(traversedIds, parameterContexts); +} + +private void verifyNoCycles(final Set traversedIds, + final List parameterContexts) { + for (final ParameterContext parameterContext : parameterContexts) { + final String id = parameterContext.getIdentifier(); + if (traversedIds.contains(id)) { // O(1) hash lookup + throw new IllegalStateException( + String.format("Circular references in Parameter Contexts not allowed. " + + "[%s] was detected in a cycle.", parameterContext.getName())); + } + traversedIds.add(id); + verifyNoCycles(traversedIds, parameterContext.getInheritedParameterContexts()); + traversedIds.remove(id); + } +} +``` + +## Speedup + +| D (chain depth) | Before (ops) | After (ops) | Speedup | +|-----------------|-------------|-------------|---------| +| 10 | 100 | 10 | 10× | +| 50 | 2,500 | 50 | 50× | +| 100 | 10,000 | 100 | 100× | +| 500 | 250,000 | 500 | 500× | diff --git a/defects/protobuf/patch/CLEAN.md b/defects/protobuf/patch/CLEAN.md new file mode 100644 index 000000000..cb31ed81d --- /dev/null +++ b/defects/protobuf/patch/CLEAN.md @@ -0,0 +1,20 @@ +# CLEAN — Protocol Buffers (Java) +Scanned 2026-03-29 for CWE-407 (algorithmic complexity). + +## Scope +Java library: java/core/src/main and java/util/src/main — Descriptors, DynamicMessage, +MessageLiteToString, JsonFormat, SchemaUtil, DescriptorMessageInfoFactory. + +## Findings + +### MessageLiteToString — setters.contains() +`setters` is `Set setters = new HashSet<>()`. O(1). Clean. + +### JsonFormat — includingDefaultValueFields.contains() +`includingDefaultValueFields` is `Set`. O(1). Clean. + +### DescriptorMessageInfoFactory — specialFieldNames.contains() +`specialFieldNames` is a static `Set`. O(1). Clean. + +## Result +**CLEAN. No actionable CWE-407 defects found in Protocol Buffers Java library.** diff --git a/defects/scala3/patch/scala3-0001-namer-export-seen-list.md b/defects/scala3/patch/scala3-0001-namer-export-seen-list.md new file mode 100644 index 000000000..d1ce8f162 --- /dev/null +++ b/defects/scala3/patch/scala3-0001-namer-export-seen-list.md @@ -0,0 +1,95 @@ +# UNDF: UNDF-2026-000000271 +# UNDF: (pending) +# scala3-0001: Namer.addWildcardForwarders — O(M×S) List[TermName] seen-scan for Given export forwarders + +## CWE-407 — Algorithmic Complexity: Inefficient Algorithmic Complexity + +| Field | Value | +|-------|-------| +| ID | scala3-0001 | +| Severity | MEDIUM | +| Ecosystem | scala3 | +| Package | dotty.tools.dotc.typer | +| File | `compiler/src/dotty/tools/dotc/typer/Namer.scala` | +| Lines | 1418–1448 | +| Complexity | O(M × S) — O(S) list scan per member for Given exports | +| Hot path | `export` clause compilation — executed once per export statement | + +## Defect + +`Namer.addWildcardForwarders` is called when an `export path.*` wildcard clause is +compiled. It receives `seen: List[TermName]` — the names of explicitly-listed selectors +that precede the wildcard in the same export clause (built by prepending in +`addForwarders`). + +For **non-Given** members the code correctly materializes `seen` into a `mutable.HashSet` +at the top of the function (`nonContextual`). But for **Given** members, it falls back to +the raw `List.contains()` O(S) scan on every member: + +```scala +def addWildcardForwarders(seen: List[TermName], span: Span): Unit = + val nonContextual = mutable.HashSet(seen*) // O(S) one-time copy — OK for non-Given branch + ... + for mbr <- pathType.membersBasedOnFlags(...) do + ... + val alias = mbr.name.toTermName + if mbr.symbol.is(Given) then + if !seen.contains(alias) && mbr.matchesImportBound(givenBound) then // BUG: O(S) per member + addForwarder(alias, mbr, span) + else if !nonContextual.contains(alias) ... // OK: O(1) +``` + +With M members in `pathType` and S explicit selectors in `seen`, the Given branch +performs **O(M × S)** comparisons. The non-Given branch already pays this cost to +construct `nonContextual`, but does it only once (O(S)) then O(1) per lookup. +The Given branch neglects to reuse `nonContextual` and re-scans the original list +per member. + +`TermName` is a value type with a stable `hashCode`, making it safe in `HashSet`. + +## Fix + +Reuse `nonContextual` (which already contains all names from `seen`) for the Given branch +lookup. Both branches then pay O(1) per member: + +```scala +def addWildcardForwarders(seen: List[TermName], span: Span): Unit = + val nonContextual = mutable.HashSet(seen*) // covers both Given and non-Given + ... + for mbr <- pathType.membersBasedOnFlags(...) do + ... + val alias = mbr.name.toTermName + if mbr.symbol.is(Given) then + if !nonContextual.contains(alias) && mbr.matchesImportBound(givenBound) then // FIX: O(1) + addForwarder(alias, mbr, span) + else if !nonContextual.contains(alias) && mbr.matchesImportBound(wildcardBound) then + nonContextual += alias + addWildcardForwardersNamed(alias, span) +``` + +The semantics are identical: `seen` contains the names of all explicit selectors, and +`nonContextual` is initialized to exactly `seen*`, so `nonContextual.contains(alias)` is +equivalent to `seen.contains(alias)` but O(1) instead of O(S). + +## Speedup + +| M (members) | S (selectors) | Before (Given ops) | After (Given ops) | Speedup | +|-------------|---------------|-------------------|-------------------|---------| +| 50 | 10 | 500 | 50 | 10× | +| 200 | 20 | 4,000 | 200 | 20× | +| 500 | 50 | 25,000 | 500 | 50× | + +Typical Scala 3 `export` clauses target trait-rich objects with O(100–500) members +and O(5–30) explicit selectors. The pathological case is a large typeclass object +with many given instances and a long list of explicit `export` selectors. + +## Affected Versions + +Scala 3.x (dotty) — `addWildcardForwarders` present since `export` was introduced +in Scala 3.0; affects all versions through current main branch. + +## References + +- Scala 3 issue tracker (none filed) +- Upstream source: `compiler/src/dotty/tools/dotc/typer/Namer.scala` +- CWE-407: https://cwe.mitre.org/data/definitions/407.html diff --git a/defects/spring/patch/spring-0001-beanfactoryutils-mergenames-list-contains.md b/defects/spring/patch/spring-0001-beanfactoryutils-mergenames-list-contains.md new file mode 100644 index 000000000..55010f716 --- /dev/null +++ b/defects/spring/patch/spring-0001-beanfactoryutils-mergenames-list-contains.md @@ -0,0 +1,102 @@ +# UNDF: UNDF-2026-000000295 +# UNDF: (pending) +# spring-0001: BeanFactoryUtils.mergeNamesWithParent — O(P×R) ArrayList.contains inside loop + +## CWE-407 — Algorithmic Complexity: Unnecessary Quadratic Complexity (List membership inside loop) + +| Field | Value | +|-------|-------| +| ID | spring-0001 | +| Severity | MEDIUM | +| Ecosystem | spring-framework | +| Package | org.springframework.beans.factory | +| File | `spring-beans/src/main/java/org/springframework/beans/factory/BeanFactoryUtils.java` | +| Lines | 525–532 | +| Complexity | O(P×R) | +| Hot path | bean type resolution with hierarchical ApplicationContext | + +## Defect + +`BeanFactoryUtils.mergeNamesWithParent()` is called by +`beanNamesForTypeIncludingAncestors()` and related methods to merge bean name +lists from parent and child application contexts. The `merged` variable is an +`ArrayList`. For each of the P entries in `parentResult`, the code calls +`merged.contains(beanName)` — an O(R) linear scan where R is the number of +already-added names. Total cost O(P×R). + +```java +// BeanFactoryUtils.java line 521-532 +private static String[] mergeNamesWithParent(String[] result, String[] parentResult, + HierarchicalBeanFactory hbf) { + if (parentResult.length == 0) { + return result; + } + List merged = new ArrayList<>(result.length + parentResult.length); + merged.addAll(Arrays.asList(result)); + for (String beanName : parentResult) { + if (!merged.contains(beanName) && !hbf.containsLocalBean(beanName)) { + // ^^^^^^^^ O(R) per iteration → O(P×R) total + merged.add(beanName); + } + } + return StringUtils.toStringArray(merged); +} +``` + +This method is called from `getBeanNamesForType()`, `beanNamesForAnnotationIncludingAncestors()`, +and similar utility methods which can be called at runtime (e.g., during dependency injection, +AOP proxy creation, and Spring Boot auto-configuration) with deep ApplicationContext hierarchies. + +## Fix + +Use a `LinkedHashSet` to preserve insertion order while providing O(1) membership tests, +or build a `HashSet` for the dedup check: + +```java +private static String[] mergeNamesWithParent(String[] result, String[] parentResult, + HierarchicalBeanFactory hbf) { + if (parentResult.length == 0) { + return result; + } + Set seen = new HashSet<>(Arrays.asList(result)); + List merged = new ArrayList<>(result.length + parentResult.length); + merged.addAll(Arrays.asList(result)); + for (String beanName : parentResult) { + if (!seen.contains(beanName) && !hbf.containsLocalBean(beanName)) { + seen.add(beanName); + merged.add(beanName); + } + } + return StringUtils.toStringArray(merged); +} +``` + +Alternatively, use `LinkedHashSet` directly: + +```java +private static String[] mergeNamesWithParent(String[] result, String[] parentResult, + HierarchicalBeanFactory hbf) { + if (parentResult.length == 0) { + return result; + } + Set merged = new LinkedHashSet<>(Arrays.asList(result)); + for (String beanName : parentResult) { + if (!merged.contains(beanName) && !hbf.containsLocalBean(beanName)) { + merged.add(beanName); + } + } + return StringUtils.toStringArray(merged); +} +``` + +## Speedup + +| R (result count) | P (parentResult count) | Before (ops) | After (ops) | Speedup | +|-----------------|------------------------|-------------|-------------|---------| +| 100 | 100 | 10,000 | 100 | 100× | +| 500 | 500 | 250,000 | 500 | 500× | +| 1,000 | 1,000 | 1,000,000 | 1,000 | 1,000× | + +Applications with large numbers of beans and deep ApplicationContext hierarchies +(common in Spring Boot multi-module applications and OSGi container deployments) +are most affected. diff --git a/defects/spring/patch/spring-0002-defaultlistablebeanfactory-getnamesforannotation-list-contains.md b/defects/spring/patch/spring-0002-defaultlistablebeanfactory-getnamesforannotation-list-contains.md new file mode 100644 index 000000000..71a6f8936 --- /dev/null +++ b/defects/spring/patch/spring-0002-defaultlistablebeanfactory-getnamesforannotation-list-contains.md @@ -0,0 +1,85 @@ +# UNDF: UNDF-2026-000000317 +# UNDF: (pending) +# spring-0002: DefaultListableBeanFactory.getBeanNamesForAnnotation — O(B×M) ArrayList.contains inside loop + +## CWE-407 — Algorithmic Complexity: Unnecessary Quadratic Complexity (List membership inside loop) + +| Field | Value | +|-------|-------| +| ID | spring-0002 | +| Severity | MEDIUM | +| Ecosystem | spring-framework | +| Package | org.springframework.beans.factory.support | +| File | `spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java` | +| Lines | 770–784 | +| Complexity | O(B×M) | +| Hot path | `getBeanNamesForAnnotation()` — called during startup and annotation-driven wiring | + +## Defect + +`DefaultListableBeanFactory.getBeanNamesForAnnotation()` builds a result list from +two sources: `beanDefinitionNames` and `manualSingletonNames`. For each of the M +entries in `manualSingletonNames` it calls `result.contains(beanName)` where `result` +is an `ArrayList` that has already been populated with up to B entries from +`beanDefinitionNames`. This is O(B×M) — quadratic in total bean count. + +```java +// DefaultListableBeanFactory.java line 770-784 +public String[] getBeanNamesForAnnotation(Class annotationType) { + List result = new ArrayList<>(); + for (String beanName : this.beanDefinitionNames) { + BeanDefinition bd = this.beanDefinitionMap.get(beanName); + if (bd != null && !bd.isAbstract() && findAnnotationOnBean(beanName, annotationType) != null) { + result.add(beanName); + } + } + for (String beanName : this.manualSingletonNames) { + if (!result.contains(beanName) && findAnnotationOnBean(beanName, annotationType) != null) { + // ^^^^^^^^ O(B) per iteration → O(B×M) total + result.add(beanName); + } + } + return StringUtils.toStringArray(result); +} +``` + +`findAnnotationOnBean()` involves reflection and is itself expensive — but it is only called +after `result.contains()` succeeds (for the negative branch), so the list scan overhead is +paid on every iteration including those that ultimately do nothing. + +## Fix + +Build a `HashSet` shadow alongside `result` to provide O(1) dedup: + +```java +public String[] getBeanNamesForAnnotation(Class annotationType) { + List result = new ArrayList<>(); + Set seen = new HashSet<>(); + for (String beanName : this.beanDefinitionNames) { + BeanDefinition bd = this.beanDefinitionMap.get(beanName); + if (bd != null && !bd.isAbstract() && findAnnotationOnBean(beanName, annotationType) != null) { + result.add(beanName); + seen.add(beanName); + } + } + for (String beanName : this.manualSingletonNames) { + if (!seen.contains(beanName) && findAnnotationOnBean(beanName, annotationType) != null) { + result.add(beanName); + } + } + return StringUtils.toStringArray(result); +} +``` + +## Speedup + +| B (bean definitions) | M (manual singletons) | Before (ops) | After (ops) | Speedup | +|---------------------|-----------------------|-------------|-------------|---------| +| 500 | 100 | 50,000 | 600 | 83× | +| 1,000 | 500 | 500,000 | 1,500 | 333× | +| 5,000 | 1,000 | 5,000,000 | 6,000 | 833× | + +Large Spring Boot applications with hundreds of beans annotated with `@Service`, +`@Component`, `@Controller` etc. will benefit most. `getBeanNamesForAnnotation()` +is called by `getBeansWithAnnotation()`, event publisher detection, and various +Spring Boot auto-configuration processors. diff --git a/defects/spring/patch/spring-0003-annotationtypemapping-processaliases-list-contains.md b/defects/spring/patch/spring-0003-annotationtypemapping-processaliases-list-contains.md new file mode 100644 index 000000000..98947fec8 --- /dev/null +++ b/defects/spring/patch/spring-0003-annotationtypemapping-processaliases-list-contains.md @@ -0,0 +1,95 @@ +# UNDF: UNDF-2026-000000296 +# UNDF: (pending) +# spring-0003: AnnotationTypeMapping.processAliases — O(A²×D×L) ArrayList.contains in nested annotation attribute loop + +## CWE-407 — Algorithmic Complexity: Unnecessary Quadratic Complexity (List membership inside nested loop) + +| Field | Value | +|-------|-------| +| ID | spring-0003 | +| Severity | MEDIUM | +| Ecosystem | spring-framework | +| Package | org.springframework.core.annotation | +| File | `spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java` | +| Lines | 197–258, 556–573 | +| Complexity | O(A²×D×L) | +| Hot path | annotation type mapping initialization (startup + first use per annotation type) | + +## Defect + +`AnnotationTypeMapping.processAliases()` resolves `@AliasFor` relationships across +meta-annotation hierarchies. It is called once per annotation type during mapping +construction (cached afterwards). The `aliases` local variable is an `ArrayList`. + +For each annotation attribute (A attributes), `processAliases(i, aliases)` is called. +Inside that method, for each level in the meta-annotation chain (D levels), it iterates +over all attributes (A) and calls `aliases.contains(mapping.attributes.get(i))` — an O(L) +scan where L is the current aliases list size. The same pattern appears in +`getFirstRootAttributeIndex()` and `MirrorSets.updateFrom()`. + +```java +// processAliases(int attributeIndex, List aliases) — lines 223-248 +AnnotationTypeMapping mapping = this; +while (mapping != null) { // D iterations (meta-annotation depth) + if (rootAttributeIndex != -1 && mapping != this.root) { + for (int i = 0; i < mapping.attributes.size(); i++) { // A iterations + if (aliases.contains(mapping.attributes.get(i))) { // O(L) scan + mapping.aliasMappings[i] = rootAttributeIndex; + } + } + } + mapping.mirrorSets.updateFrom(aliases); // updateFrom also calls aliases.contains() + ... + mapping = mapping.source; +} + +// MirrorSets.updateFrom(Collection aliases) — lines 556-573 +for (int i = 0; i < attributes.size(); i++) { // A iterations + Method attribute = attributes.get(i); + if (aliases.contains(attribute)) { // O(L) scan + ... + } +} +``` + +Total cost per annotation type: O(A × D × A × L) = O(A²×D×L). + +For complex composed annotations (e.g., `@SpringBootTest`, `@Transactional`, framework +stereotypes with `@AliasFor` chains), A can be 10–30, D can be 5–10, L can be 5–10, +yielding up to ~90,000 operations per annotation type initialization. + +## Fix + +Replace `List` with `LinkedHashSet` for `aliases` to make `.contains()` +O(1) while preserving insertion order (order matters for alias resolution): + +```java +private void processAliases() { + Set aliases = new LinkedHashSet<>(); // was: List aliases = new ArrayList<>(); + for (int i = 0; i < this.attributes.size(); i++) { + aliases.clear(); + aliases.add(this.attributes.get(i)); + collectAliases(aliases); + if (aliases.size() > 1) { + processAliases(i, aliases); + } + } +} +``` + +The `processAliases(int, List)` signature must also accept `Collection` +or `Set` (or keep `List` and pass `new ArrayList<>(aliases)`). +Similarly update `getFirstRootAttributeIndex()` and `MirrorSets.updateFrom()` parameter +types to accept `Collection`. + +## Speedup + +| A (attributes) | D (depth) | L (alias list) | Before (ops) | After (ops) | Speedup | +|---------------|-----------|---------------|-------------|-------------|---------| +| 10 | 5 | 5 | 2,500 | 500 | 5× | +| 20 | 8 | 10 | 32,000 | 4,000 | 8× | +| 30 | 10 | 15 | 135,000 | 9,000 | 15× | + +Annotation types are cached after first construction, so this is a startup / first-use +cost rather than per-request. Applications with many composed annotations (Spring Boot, +Spring Security, Spring Data) construct many such mappings during startup. diff --git a/defects/thrift/patch/CLEAN.md b/defects/thrift/patch/CLEAN.md index b876df6a1..fb0dc973c 100644 --- a/defects/thrift/patch/CLEAN.md +++ b/defects/thrift/patch/CLEAN.md @@ -1,20 +1,19 @@ -# CLEAN — Apache Thrift -Scanned 2026-03-29 for CWE-407. +# CLEAN — Apache Thrift (Java) +Scanned 2026-03-29 for CWE-407 (algorithmic complexity). ## Scope - -- `lib/java/src/main/java` — Java runtime library (PartialThriftComparer, TBase implementations) -- `compiler/cpp/src/thrift/parse/` — C++ IDL compiler (t_scope.h, t_program.h, t_const_value.h) +Library: lib/java/src — TBinaryProtocol, TCompactProtocol, TJSONProtocol, partial Thrift +comparer, service processor infrastructure. ## Findings -| Location | Pattern | Type | Result | -|----------|---------|------|--------| -| `PartialThriftComparer.areEqual` (Set path) | `s2.contains(e1)` in loop over s1 | `s2` is `Set` (Java Set semantics per Thrift spec) | CLEAN | -| `t_scope.h` | `types_.find`, `services_.find`, `constants_.find` | `std::map` — O(log N) | CLEAN | -| `t_program.h` | namespace lookups | `std::map` | CLEAN | -| `contrib/thrift-maven-plugin` | `thriftPathElements.contains` | Called once per directory during classpath building, not in hot loop | LOW | +### partial/PartialThriftComparer — s2.contains() +`s2` is cast from parameter `Object o2` and the cast target is `Set`: +```java +Set s2 = (Set) o2; +if (!s2.contains(e1)) { ... } +``` +O(1) set membership. Clean. -The Java library is minimal and the C++ compiler uses `std::map` (ordered map, O(log N) find). No quadratic list-membership pattern exists in the production hot path. - -**Result: No actionable CWE-407 defects.** +## Result +**CLEAN. No actionable CWE-407 defects found in Apache Thrift (Java library).** diff --git a/defects/tor/patch/CLEAN.md b/defects/tor/patch/CLEAN.md index 38102c6bb..d9fc4e5bc 100644 --- a/defects/tor/patch/CLEAN.md +++ b/defects/tor/patch/CLEAN.md @@ -1,21 +1,22 @@ -# CLEAN — Tor Anonymity Network -Scanned 2026-03-29 for CWE-407 (algorithmic complexity: O(N²) linear membership tests, O(2^D) diamond recursion). +# CLEAN — Tor Anonymity Network (beyond tor-0001/0002/0003) +Scanned 2026-03-29 for additional CWE-407 defects beyond existing patches. -## Scope -- `src/feature/client/entrynodes.c` — guard node selection, sampled/confirmed guard lists -- `src/feature/client/circpathbias.c` — circuit path bias tracking -- `src/feature/nodelist/node_select.c` — node selection with exclusion lists -- `src/feature/nodelist/nodelist.c` — family membership, `nodes_have_common_family_id` -- `src/feature/nodelist/routerlist.c` — router descriptor ingestion -- `src/feature/hs/hs_service.c` — hidden service hsdir tracking -- `src/feature/relay/dns.c` — DNS wildcard detection +## Existing Defects (already patched) +- `tor-0001` — `router_load_routers_from_string` requested_fingerprints smartlist O(R²) → digestmap +- `tor-0002` — `nodes_have_common_family_id` family ID string scan → strmap +- `tor-0003` — KIST scheduler re-add heap index O(N) → O(1) + +## Additional Areas Scanned +- `src/feature/client/entrynodes.c` — guard selection, sampled/confirmed guard lists +- `src/feature/nodelist/node_select.c` — node exclusion via bitarray (already O(N)) +- `src/feature/hs/hs_service.c` — previous_hsdirs tracking (bounded ≤6 entries) +- `src/feature/relay/dns.c` — dns_wildcard_list scan (bounded ≤10 entries) +- `src/feature/client/circpathbias.c` — circuit list scan (one-off at circuit close) ## Findings -- **`nodelist_subtract`** (`node_select.c:890`) — explicitly noted in comments as "delivers linear performance when smartlist_subtract would be quadratic." Uses `bitarray_t` indexed by `node->nodelist_idx`. O(N+M). CLEAN. -- **`nodes_have_common_family_id`** (`nodelist.c:2190`) — O(|ids_a| × |ids_b|) nested loop over family certificate IDs. Both lists are tiny (typically 1–3 IDs per relay). Outer loop in `nodelist_add_node_and_family` iterates all ~7000 relays but inner product is O(F²) where F≪1. Not a scalable O(N²). CLEAN. -- **`entrynodes.c` guard lists** — `smartlist_contains` on `primary_entry_guards` and `confirmed_entry_guards`: bounded by `MAX_SAMPLE_THRESHOLD` (dozens), called in setup/error paths (BUG assertions), not hot per-circuit. CLEAN. -- **`hs_service.c` `previous_hsdirs`** — `smartlist_contains_string` on a list bounded by `REND_NUMBER_OF_CONSECUTIVE_REPLICAS` (6). CLEAN. -- **`dns.c` `dns_wildcard_list`** — linear scan on a list of hijacked IPs. In practice contains 0–10 entries; called once per DNS response. CLEAN. -- **Router descriptor ingestion** — `requested_fingerprints` shrinks as each router is processed (O(N) total). CLEAN. +- **`nodelist_subtract`** — already uses `bitarray_t` for O(N) subtraction. CLEAN. +- **`entrynodes.c` guard BUG-checks** — constant-bounded (MAX_SAMPLE_THRESHOLD dozens). CLEAN. +- **`hs_service.c` previous_hsdirs** — bounded by `REND_NUMBER_OF_CONSECUTIVE_REPLICAS` (6). CLEAN. +- **`dns.c` wildcard list** — bounded to ~10 hijacked IPs. CLEAN. -**Result: No actionable CWE-407 defects. Tor's hot node-selection paths use bitarray-indexed O(N) exclusion; all family/guard list scans operate on constant-bounded collections.** +**Result: No additional CWE-407 defects beyond existing patches.** diff --git a/defects/trino/patch/trino-0001-join-using-list-contains-bitset.md b/defects/trino/patch/trino-0001-join-using-list-contains-bitset.md new file mode 100644 index 000000000..1e797356d --- /dev/null +++ b/defects/trino/patch/trino-0001-join-using-list-contains-bitset.md @@ -0,0 +1,93 @@ +# UNDF: UNDF-2026-000000560 +# UNDF: (pending) +# trino-0001: StatementAnalyzer JOIN USING — O(C×J) ArrayList.contains for field dedup + +## CWE-407 — Algorithmic Complexity + +| Field | Value | +|-------|-------| +| ID | trino-0001 | +| Severity | MEDIUM | +| Ecosystem | trino | +| Package | trino-main | +| File | `core/trino-main/src/main/java/io/trino/sql/analyzer/StatementAnalyzer.java` | +| Lines | 3975–4030 | +| Complexity | O(C×J) | +| Hot path | Called for every SQL JOIN ... USING (...) query during analysis | + +## Defect + +When analyzing a `JOIN ... USING (col1, col2, ...)` clause, two `ArrayList` lists +(`leftJoinFields`, `rightJoinFields`) are built from the USING column indices, then used as +membership test targets in loops over all fields of the left and right relation types: + +```java +List leftJoinFields = new ArrayList<>(); +List rightJoinFields = new ArrayList<>(); + +// build phase: O(J) +for (Identifier column : columns) { + leftJoinFields.add(leftField.getRelationFieldIndex()); + rightJoinFields.add(rightField.getRelationFieldIndex()); +} + +// output phase: O(C × J) -- for each field, scan the list +for (int i = 0; i < left.getRelationType().getAllFieldCount(); i++) { + if (!leftJoinFields.contains(i)) { // O(J) linear scan per field + outputs.add(...); leftFields.add(i); + } +} +for (int i = 0; i < right.getRelationType().getAllFieldCount(); i++) { + if (!rightJoinFields.contains(i)) { // O(J) linear scan per field + outputs.add(...); rightFields.add(i); + } +} +``` + +Where C = total column count across both sides, J = number of USING columns. For wide tables +(C large) with many join keys (J large), this degrades to O(C²) in the worst case. + +## Fix + +Replace `ArrayList` with `HashSet` for O(1) membership lookup. The lists are +only used for contains checks in the output phase; the ordering is not required for that. + +```java +Set leftJoinFields = new HashSet<>(); +Set rightJoinFields = new HashSet<>(); + +// build phase +for (Identifier column : columns) { + leftJoinFields.add(leftField.getRelationFieldIndex()); + rightJoinFields.add(rightField.getRelationFieldIndex()); + joinFields.add(Field.newUnqualified(column.getValue(), type.get())); + // ... other logic unchanged +} + +// output phase: O(C) total +for (int i = 0; i < left.getRelationType().getAllFieldCount(); i++) { + if (!leftJoinFields.contains(i)) { // O(1) hash lookup + outputs.add(left.getRelationType().getFieldByIndex(i)); + leftFields.add(i); + } +} +for (int i = 0; i < right.getRelationType().getAllFieldCount(); i++) { + if (!rightJoinFields.contains(i)) { // O(1) hash lookup + outputs.add(right.getRelationType().getFieldByIndex(i)); + rightFields.add(i); + } +} +// Pass sets to JoinUsingAnalysis (also accepts Collection) +analysis.setJoinUsing(node, new Analysis.JoinUsingAnalysis( + ImmutableList.copyOf(leftJoinFields), ImmutableList.copyOf(rightJoinFields), + leftFields.build(), rightFields.build())); +``` + +## Speedup + +| C (columns) | J (join keys) | Before (ops) | After (ops) | Speedup | +|-------------|--------------|-------------|-------------|---------| +| 50 | 5 | 250 | 50 | 5× | +| 200 | 20 | 4,000 | 200 | 20× | +| 500 | 50 | 25,000 | 500 | 50× | +| 1,000 | 100 | 100,000 | 1,000 | 100× | diff --git a/defects/vertx-core/patch/vertx-core-0001-hamanager-nodeleft-nodes-list-contains.md b/defects/vertx-core/patch/vertx-core-0001-hamanager-nodeleft-nodes-list-contains.md new file mode 100644 index 000000000..6da52e607 --- /dev/null +++ b/defects/vertx-core/patch/vertx-core-0001-hamanager-nodeleft-nodes-list-contains.md @@ -0,0 +1,73 @@ +# UNDF: UNDF-2026-000000233 +# UNDF: (pending) +# vertx-core-0001: HAManager.nodeLeft — O(N×M) nodes List.contains inside clusterMap loop + +## CWE-407 — Algorithmic Complexity: Unnecessary Quadratic Complexity (List membership inside loop) + +| Field | Value | +|-------|-------| +| ID | vertx-core-0001 | +| Severity | HIGH | +| Ecosystem | vertx-core | +| Package | io.vertx.core.impl | +| File | `vertx-core/src/main/java/io/vertx/core/impl/HAManager.java` | +| Lines | 307–314 | +| Complexity | O(N×M) | +| Hot path | every cluster node departure event | + +## Defect + +`HAManager.nodeLeft()` is called whenever a node leaves a Vert.x cluster. It +resumes any prior failed failovers by scanning `clusterMap` for entries not in +the current live node list. The live node list is obtained from +`clusterManager.getNodes()` which returns a `List`. For each of the N +entries in `clusterMap`, it calls `nodes.contains(entry.getKey())` — an O(M) +linear scan of the node list. With N cluster-map entries and M live nodes the +total cost is O(N×M) per node-departure event. + +```java +// HAManager.java line 307-314 +List nodes = clusterManager.getNodes(); // returns List + +for (Map.Entry entry: clusterMap.entrySet()) { + if (!leftNodeID.equals(entry.getKey()) && !nodes.contains(entry.getKey())) { + // ^^^^^^^^ O(M) per iteration + JsonObject haInfo = new JsonObject(entry.getValue()); + checkFailover(entry.getKey(), haInfo); + } +} +``` + +The same pattern appears in `addHaInfoIfLost()` (line 319): +```java +if (clusterManager.getNodes().contains(nodeID) && ...) // O(M) single call, low risk +``` + +## Fix + +Convert the node list to a `HashSet` before the loop so membership checks are O(1). + +```java +Set nodesSet = new HashSet<>(clusterManager.getNodes()); + +for (Map.Entry entry: clusterMap.entrySet()) { + if (!leftNodeID.equals(entry.getKey()) && !nodesSet.contains(entry.getKey())) { + JsonObject haInfo = new JsonObject(entry.getValue()); + checkFailover(entry.getKey(), haInfo); + } +} +``` + +For `addHaInfoIfLost()` the single call is not in a loop so is low risk — but can be converted similarly if consistency is desired. + +## Speedup + +| N (clusterMap entries) | M (nodes) | Before (ops) | After (ops) | Speedup | +|------------------------|-----------|-------------|-------------|---------| +| 100 | 100 | 10,000 | 100 | 100× | +| 500 | 500 | 250,000 | 500 | 500× | +| 1,000 | 1,000 | 1,000,000 | 1,000 | 1,000× | + +In large Vert.x HA clusters with many concurrent deployments the quadratic +cost is paid on every node failure event — exactly when latency matters most +for failover speed.