3.5 KiB
3.5 KiB
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:
// 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:
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:
// 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).