swift-0001: QualifiedLookupRequest::evaluate (NameLookup.cpp:2860) pushes protocol superclassDecl onto BFS stack unconditionally — visited.insert() return not checked. Nearby ClassDecl arm (line 2833) is correctly guarded. Diamond protocol hierarchies sharing a class superclass trigger O(2^D) re-traversal on every qualified member lookup. Fix: check .second. llvm, gcc, webkit: CLEAN in available sparse-clone code.
4.5 KiB
UNDF: (pending)
swift-0001: QualifiedLookupRequest — O(2^D) diamond re-traversal via unguarded protocol superclass push
CWE-407 — Algorithmic Complexity: O(2^D) recursive diamond re-traversal
| Field | Value |
|---|---|
| ID | swift-0001 |
| Severity | MEDIUM |
| Ecosystem | swift |
| Package | lib/AST |
| File | lib/AST/NameLookup.cpp |
| Lines | 2858–2861 |
| Complexity | O(2^D) on diamond protocol+class hierarchies |
| Hot path | qualified name lookup (every member access, every type check) |
Defect
QualifiedLookupRequest::evaluate (NameLookup.cpp) walks a nominal type's
inheritance graph via a BFS worklist. When the current node is a
ProtocolDecl that has a class superclass, lines 2858–2861 push that
superclass onto the stack without checking whether it was already
visited:
// lib/AST/NameLookup.cpp lines 2858–2861 (DEFECT)
if (auto superclassDecl = protoDecl->getSuperclassDecl()) {
visited.insert(superclassDecl); // .second NOT checked
stack.push_back(superclassDecl); // unconditional push → O(2^D)
}
The identical operation 27 lines earlier (line 2833–2834, for the
ClassDecl arm) is written correctly:
// line 2833–2834 (CORRECT — nearby, same function)
if (visited.insert(superclassDecl).second)
stack.push_back(superclassDecl);
Diamond scenario
Consider a protocol diamond where two protocols both have the same class
superclass Base:
protocol PA: Base {} // PA.getSuperclassDecl() == Base
protocol PB: Base {} // PB.getSuperclassDecl() == Base
protocol P: PA, PB {} // inherits both
When looking up a member on P:
- BFS pops
P→ enqueuesPA,PBviaaddNominalType(correctly guarded). - BFS pops
PA→protoDecl->getSuperclassDecl()returnsBase.visited.insert(Base)succeeds,Basepushed. stack = [PB, Base] - BFS pops
Base→ processed normally. - BFS pops
PB→protoDecl->getSuperclassDecl()returnsBaseagain.visited.insert(Base)silently returns{iter, false}(already present) but the return value is ignored →Basepushed again. stack = [Base] —Basere-processed.
For a depth-D diamond this re-visits Base 2^D times.
Hot path: QualifiedLookupRequest is evaluated on every qualified member
expression (.foo, .bar), every protocol conformance check, and every
type-checker constraint solve. In large codebases with deep protocol
hierarchies this fires frequently per compilation.
Fix
// lib/AST/NameLookup.cpp lines 2858–2861 (AFTER — O(D))
if (auto superclassDecl = protoDecl->getSuperclassDecl()) {
if (visited.insert(superclassDecl).second) // guard: only if newly inserted
stack.push_back(superclassDecl);
}
One-line change matching the existing correct pattern on line 2833.
Speedup
| Diamond depth (D) | Shared superclass visits (before) | After | Speedup |
|---|---|---|---|
| 2 | 2 | 1 | 2× |
| 5 | 16 | 1 | 16× |
| 10 | 512 | 1 | 512× |
| 15 | 16,384 | 1 | 16,384× |
(Speedup is specifically for the superclass re-traversal component; total lookup speedup depends on hierarchy breadth.)
Context
// Full function context: lib/AST/NameLookup.cpp
QualifiedLookupResult
QualifiedLookupRequest::evaluate(Evaluator &eval, const DeclContext *DC,
SmallVector<NominalTypeDecl *, 4> typeDecls,
DeclNameRef member, NLOptions options) const {
SmallVector<NominalTypeDecl *, 4> stack;
llvm::SmallPtrSet<NominalTypeDecl *, 4> visited; // line 2753
auto addNominalType = [&](NominalTypeDecl *nominal) {
if (!visited.insert(nominal).second) // CORRECT guard
return false;
stack.push_back(nominal);
return true;
};
...
while (!stack.empty()) {
auto current = stack.back();
stack.pop_back();
...
if (auto classDecl = dyn_cast<ClassDecl>(current)) {
if (visitSuperclass) {
if (auto superclassDecl = classDecl->getSuperclassDecl())
if (visited.insert(superclassDecl).second) // line 2833: CORRECT
stack.push_back(superclassDecl);
}
}
...
if (auto *protoDecl = dyn_cast<ProtocolDecl>(current)) {
if (!sawClassDecl) {
if (auto superclassDecl = protoDecl->getSuperclassDecl()) {
visited.insert(superclassDecl); // line 2860: DEFECT — .second not checked
stack.push_back(superclassDecl); // line 2861: unconditional push
}
}
}
}
}