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:
russell@unturf.com 2026-03-29 16:52:04 -04:00
parent ebfcdd3db5
commit 3986d8dc50
46 changed files with 2339 additions and 1 deletions

View file

@ -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).

View file

@ -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;
}
}

View 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.