java-topology/defects/dagger/patch/dagger-0001-legacybindinggraphfactory-cyclestack-deque-contains.md

100 lines
3.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# UNDF: UNDF-2026-000000580
# UNDF: (pending)
# dagger-0001: LegacyBindingGraphFactory.resolve — cycleStack Deque.contains() O(N) cycle detection
## CWE-407 — Algorithmic Complexity: O(N) Deque membership scan in recursive dependency resolution
| Field | Value |
|--------------|-------|
| ID | dagger-0001 |
| Severity | MEDIUM |
| Ecosystem | dagger |
| Package | dagger-compiler |
| File | `dagger-compiler/main/java/dagger/internal/codegen/binding/LegacyBindingGraphFactory.java` |
| Lines | 312, 512, 723 |
| Complexity | O(N) per call; O(N×D) total over depth-D resolution chain |
| Hot path | Called during Dagger component binding resolution (annotation processing) |
## Defect
`LegacyBindingGraphFactory.Resolver.resolve(Key)` uses a `Deque<Key> cycleStack` for cycle detection.
`Deque.contains()` is O(N) — it performs a linear scan over all elements in the deque:
```java
// LegacyBindingGraphFactory.java:312
final Deque<Key> cycleStack = new ArrayDeque<>();
// LegacyBindingGraphFactory.java:720-740 (DEFECT)
void resolve(Key key) {
if (cycleStack.contains(key)) { // O(N) linear scan of Deque
return;
}
if (resolvedContributionBindings.containsKey(key)) { // O(1) — HashMap
return;
}
cycleStack.push(key);
try {
LegacyResolvedBindings bindings = lookUpBindings(key);
resolvedContributionBindings.put(key, bindings);
resolveDependencies(bindings); // recurses
} finally {
cycleStack.pop();
}
}
```
Also at line 512 in `createDelegateBinding`:
```java
if (cycleStack.contains(delegateKey)) { // O(N) linear scan of Deque
return bindingFactory.unresolvedDelegateBinding(delegateDeclaration);
}
```
For a dependency graph of N bindings with max chain depth D, `cycleStack.contains()` is called N times
at an average depth of D/2, giving O(N×D) total comparisons instead of O(N).
For a large Dagger component with 200 bindings in a chain of depth 20:
- Before: 200 × 10 = 2,000 contains-comparisons
- After: 200 × 1 = 200 contains-comparisons (O(1) HashSet lookup)
## Fix
Add a parallel `Set<Key> cycleSet` that mirrors the Deque's membership for O(1) contains:
```java
// AFTER — O(1) cycle detection
final Deque<Key> cycleStack = new ArrayDeque<>();
final Set<Key> cycleSet = new HashSet<>(); // ADD: mirrors cycleStack membership
void resolve(Key key) {
if (cycleSet.contains(key)) { // O(1) HashSet lookup
return;
}
if (resolvedContributionBindings.containsKey(key)) {
return;
}
cycleStack.push(key);
cycleSet.add(key); // ADD: keep mirror in sync
try {
LegacyResolvedBindings bindings = lookUpBindings(key);
resolvedContributionBindings.put(key, bindings);
resolveDependencies(bindings);
} finally {
cycleStack.pop();
cycleSet.remove(key); // ADD: remove on pop
}
}
```
Same fix for `createDelegateBinding` line 512: replace `cycleStack.contains(delegateKey)` with
`cycleSet.contains(delegateKey)`.
## Speedup
| Bindings (N) × Depth (D) | Before (comparisons) | After (comparisons) | Speedup |
|--------------------------|----------------------|---------------------|---------|
| 50 × 10 | 250 | 50 | 5× |
| 100 × 20 | 1,000 | 100 | 10× |
| 200 × 50 | 5,000 | 200 | 25× |
Growth before: O(N×D). Growth after: O(N).