diamond hunt: godot-0009/0010 + meson-0002 + typeorm-0004/0005 + ts-0003; count 629→635
New diamond recursion defects (O(2^D) → O(N)): - godot-0009: Font::_is_cyclic no visited set — CJK fallback diamond, 2648x at F=4,D=8 - godot-0010: Font::_update_rids_fb no visited set — duplicate RIDs + O(N^2) hot path - meson-0002: get_internal_static_libraries_recurse link_whole guard missing — 132x at D=10 - typescript-0003: hasBaseType inner check() no visited set — 1024x at D=10; hot on instanceof New O(N²) defects: - typeorm-0004: SubjectTopologicalSorter Array.indexOf dedup — 200x at N=400 - typeorm-0005: DepGraph.createDFS result.indexOf + addDependency edge dedup — 300x at N=600 CLEAN confirmed (diamond recursion sweep): bazel, cargo, cmake, composer, dgl, diesel, doctrine-orm, efcore, helm, mybatis, networkx-deeper, ninja, npm-arborist, peewee, pip, rubygems, seaorm, sqlalchemy, swift UNDF: 571→578 assigned; MOAD count: 629→635
This commit is contained in:
parent
ebfcdd3db5
commit
3986d8dc50
46 changed files with 2339 additions and 1 deletions
|
|
@ -0,0 +1,100 @@
|
|||
# UNDF: UNDF-2026-000000441
|
||||
## Classification
|
||||
|
||||
| Field | Value |
|
||||
|-------------|-------|
|
||||
| CWE | CWE-407 Inefficient Algorithmic Complexity |
|
||||
| Severity | HIGH |
|
||||
| Component | `src/compiler/checker.ts:13029` |
|
||||
| Function | `hasBaseType` / inner `check()` |
|
||||
| Hot path | Type narrowing (instanceof), type compatibility, union reduction — called on every `instanceof` check and every type subtype query involving class/interface hierarchies |
|
||||
| Status | PATCHED (unit test PASS) |
|
||||
|
||||
## Defect
|
||||
|
||||
`hasBaseType` contains an inner recursive function `check()` that traverses
|
||||
the class/interface base-type graph with **no visited set**. TypeScript
|
||||
interfaces support multiple inheritance (`interface A extends B, C {}`), so
|
||||
the base-type graph is a DAG, not a tree. On a diamond — four types
|
||||
`A extends B,C; B extends D; C extends D` — `check(D)` is evaluated
|
||||
**twice**. At depth D the call count is **2^D**.
|
||||
|
||||
```typescript
|
||||
// src/compiler/checker.ts:13029
|
||||
function hasBaseType(type: Type, checkBase: Type | undefined) {
|
||||
return check(type);
|
||||
function check(type: Type): boolean {
|
||||
if (getObjectFlags(type) & (ObjectFlags.ClassOrInterface | ObjectFlags.Reference)) {
|
||||
const target = getTargetType(type) as InterfaceType;
|
||||
// getBaseTypes() caches the resolved base array on `target`,
|
||||
// but check() itself has NO per-node result cache.
|
||||
// On a diamond, inner nodes are re-entered 2^(their depth) times.
|
||||
return target === checkBase || some(getBaseTypes(target), check);
|
||||
}
|
||||
else if (type.flags & TypeFlags.Intersection) {
|
||||
return some((type as IntersectionType).types, check);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Call sites (hot paths):**
|
||||
|
||||
| Line | Caller | When triggered |
|
||||
|------|--------|----------------|
|
||||
| 13377 | `resolveBaseTypesOfClass` | class base resolution (compile-time) |
|
||||
| 13426 | `resolveBaseTypesOfInterface` | interface base resolution (compile-time) |
|
||||
| 21334 | `isTypeDerivedFrom` | `instanceof` narrowing, every instanceof expression |
|
||||
| 25226 | `checkPropertyAccessibility` | property access on class instances |
|
||||
| 25242 | `isClassDerivedFromDeclaringClasses` | protected property access |
|
||||
| 34614 | `getContainingClass` | various declaration checks |
|
||||
|
||||
The hottest path is `isTypeDerivedFrom` at line 21334, called for every
|
||||
`instanceof` type guard and union/intersection reduction. In a codebase with
|
||||
a 5-level diamond interface hierarchy (realistic in large TS frameworks with
|
||||
mixin patterns), `hasBaseType` performs 2^5 = 32 redundant calls to `check()`
|
||||
per type query.
|
||||
|
||||
**Blowup table (diamond depth D):**
|
||||
|
||||
| D (depth) | Nodes visited | Without fix |
|
||||
|-----------|---------------|-------------|
|
||||
| 3 | 4 | 8 calls |
|
||||
| 5 | 6 | 32 calls |
|
||||
| 7 | 8 | 128 calls |
|
||||
| 10 | 11 | 1,024 calls |
|
||||
|
||||
In pathological but valid TypeScript (generated code, DTO hierarchies,
|
||||
mixin tower patterns), D can reach 10+.
|
||||
|
||||
## Fix
|
||||
|
||||
Add a `Set<Type>` (keyed by the `target` canonical type) to `check()` so each
|
||||
node is visited at most once:
|
||||
|
||||
```typescript
|
||||
function hasBaseType(type: Type, checkBase: Type | undefined) {
|
||||
// CWE-407 fix: memoize to avoid 2^D traversal on diamond interface graphs
|
||||
const seen = new Set<Type>();
|
||||
return check(type);
|
||||
function check(type: Type): boolean {
|
||||
if (getObjectFlags(type) & (ObjectFlags.ClassOrInterface | ObjectFlags.Reference)) {
|
||||
const target = getTargetType(type) as InterfaceType;
|
||||
if (target === checkBase) return true;
|
||||
if (seen.has(target)) return false;
|
||||
seen.add(target);
|
||||
return some(getBaseTypes(target), check);
|
||||
}
|
||||
else if (type.flags & TypeFlags.Intersection) {
|
||||
return some((type as IntersectionType).types, check);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Complexity: **O(2^D) → O(N)** where N = number of distinct types in the
|
||||
reachable base-type DAG.
|
||||
|
||||
Speedup at D=10: ~1,024x (1024 calls → 11 calls).
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
# UNDF: UNDF-2026-000000441
|
||||
--- a/src/compiler/checker.ts
|
||||
+++ b/src/compiler/checker.ts
|
||||
@@ -13026,13 +13026,17 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
|
||||
// TODO: GH#18217 If `checkBase` is undefined, we should not call this because this will always return false.
|
||||
function hasBaseType(type: Type, checkBase: Type | undefined) {
|
||||
+ // CWE-407 fix: memoize visited targets to avoid O(2^D) traversal on
|
||||
+ // diamond interface graphs. Without this, each shared ancestor is
|
||||
+ // re-entered 2^(its depth) times (exponential blowup).
|
||||
+ const seen = new Set<Type>();
|
||||
return check(type);
|
||||
function check(type: Type): boolean {
|
||||
if (getObjectFlags(type) & (ObjectFlags.ClassOrInterface | ObjectFlags.Reference)) {
|
||||
const target = getTargetType(type) as InterfaceType;
|
||||
- return target === checkBase || some(getBaseTypes(target), check);
|
||||
+ if (target === checkBase) return true;
|
||||
+ if (seen.has(target)) return false;
|
||||
+ seen.add(target);
|
||||
+ return some(getBaseTypes(target), check);
|
||||
}
|
||||
else if (type.flags & TypeFlags.Intersection) {
|
||||
return some((type as IntersectionType).types, check);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
27
defects/typescript/patch/typescript-deeper-diamond-CLEAN.md
Normal file
27
defects/typescript/patch/typescript-deeper-diamond-CLEAN.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
## Diamond Recursion Scan — Deeper Scan Notes
|
||||
|
||||
**Scan date:** 2026-03-29
|
||||
**Pattern:** Recursive cycle/dependency check without visited set (CWE-407 diamond recursion, O(2^D))
|
||||
|
||||
### Summary
|
||||
|
||||
**typescript-0001 (existing)** covers `findResolutionCycleStartIndex` O(depth) list scan, `getExportsOfModuleWorker` visitedSymbols array O(N²), `getAccessibleSymbolChainFromSymbolTable` visitedSymbolTables array O(N²).
|
||||
|
||||
**typescript-0002 FOUND** in `hasBaseType` inner `check()` function — diamond interface hierarchy causes O(2^D) traversal. See `ts-0002-hasbasetype-diamond-recursion.md`.
|
||||
|
||||
### Other functions checked — CLEAN
|
||||
|
||||
- `getBaseTypes` (line 13296): caches result in `type.resolvedBaseTypes` after first computation — CLEAN for repeated calls to `getBaseTypes(sameType)`.
|
||||
- `getResolvedBaseConstraint` / `getImmediateBaseConstraint` (line 15330): uses `pushTypeResolution`/`popTypeResolution` stack guard — CLEAN.
|
||||
- `resolveBaseTypesOfClass` / `resolveBaseTypesOfInterface`: called once per type (guarded by `type.baseTypesResolved`) — CLEAN for resolution itself.
|
||||
- `isCircularMappedProperty` (line 32381): checks single property, not a traversal — CLEAN.
|
||||
- `hasNonCircularTypeParameterDefault` (line 15509): uses `pushTypeResolution` guard — CLEAN.
|
||||
- `isReachableFlowNode` (line 28865): uses `FlowNode` state flags — CLEAN.
|
||||
|
||||
### GHC note (adjacent)
|
||||
|
||||
GHC's `transSuperClasses` in `TcType.hs` uses `rec_clss` (NameSet) to prevent cycles
|
||||
but NOT to prevent re-visiting shared ancestors in diamond type-class hierarchies. The
|
||||
`rec_clss` is passed down per-branch (not accumulated across siblings), meaning shared
|
||||
superclasses in a diamond ARE visited twice. In practice Haskell class hierarchies are
|
||||
shallow enough that this is not a practical concern. No new GHC defect filed.
|
||||
Loading…
Add table
Add a link
Reference in a new issue