Fix stack overflow in relater's skipCaching path for recursive tuple types#3171
Fix stack overflow in relater's skipCaching path for recursive tuple types#3171
Conversation
…ements Add depth limit to the relater's skipCaching path in isRelatedToEx to prevent infinite recursion when comparing recursive union types with fewer than 4 constituents. The skipCaching optimization bypassed recursiveTypeRelatedTo's existing depth protection, causing unbounded recursion through unionOrIntersectionRelatedTo → eachTypeRelatedToType → isRelatedToEx. Also add a depth limit to inferFromTypes as a safety measure against potential infinite recursion in the inference code path. Fixes microsoft/TypeScript#63270 Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com> Agent-Logs-Url: https://github.com/microsoft/typescript-go/sessions/f12a8fa0-b738-405a-a4c2-67edd64ba015
|
@ahejlsberg this seems like two fixes when only one is needed, but I'm not sure which is preferable (or maybe there's a third option) |
There was a problem hiding this comment.
Pull request overview
Prevents stack overflows in the type-relater/inference recursion paths when caching is bypassed (e.g., recursive tuple types with spreads), ensuring the compiler reports “excessive depth” diagnostics instead of crashing.
Changes:
- Add an
overallDepthguard inRelater.isRelatedToExfor theskipCachingpath to stop unbounded recursion. - Add a
depthguard ininferFromTypesfor defense-in-depth against recursive inference loops. - Add a compiler regression test and baselines for recursive tuple type with spread overflow.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| testdata/tests/cases/compiler/recursiveTupleTypeWithSpreadOverflow.ts | Adds a repro case to ensure the compiler doesn’t crash on recursive tuple/spread types. |
| testdata/baselines/reference/compiler/recursiveTupleTypeWithSpreadOverflow.types | Captures expected type baseline output for the new test. |
| testdata/baselines/reference/compiler/recursiveTupleTypeWithSpreadOverflow.symbols | Captures expected symbol baseline output for the new test. |
| testdata/baselines/reference/compiler/recursiveTupleTypeWithSpreadOverflow.errors.txt | Captures expected error output (including excessive depth diagnostics). |
| internal/checker/relater.go | Introduces overallDepth limiting in skipCaching relatedness checks. |
| internal/checker/inference.go | Introduces a recursion depth limiter for inference to avoid unbounded recursion. |
| if n.depth > 200 { | ||
| n.inferencePriority = min(n.inferencePriority, InferencePriorityCircularity) | ||
| return | ||
| } | ||
| n.depth++ | ||
| defer func() { n.depth-- }() |
There was a problem hiding this comment.
The PR description says there is a depth limit of 200 in inferFromTypes, but this check happens before the increment, which effectively allows reaching a depth of 201 (the 201st call passes when n.depth == 200, increments to 201, and proceeds). If the intended hard cap is 200 active frames, increment first and then compare, or change the pre-check to >= 200 to align behavior with the stated limit.
| r.overallDepth++ | ||
| if r.overallDepth > 100 { | ||
| r.overflow = true | ||
| result = TernaryFalse | ||
| } else { | ||
| result = r.unionOrIntersectionRelatedTo(source, target, reportErrors, intersectionState) | ||
| } | ||
| r.overallDepth-- |
There was a problem hiding this comment.
The overallDepth-- is manually paired with the increment; converting the decrement to a defer immediately after increment would make this depth accounting more robust against future edits (e.g., additional early returns added in this block) and maintain invariants more reliably. Also, consider replacing the magic number 100 with a named constant (ideally shared/derived from the existing recursion-depth limit used by recursiveTypeRelatedTo) to keep these limits consistent over time.
Recursive union types with fewer than 4 constituents (e.g., recursive tuple types with spreads) trigger unbounded recursion through the
skipCachingpath inisRelatedToEx, which bypassesrecursiveTypeRelatedTo's depth protection entirely.The cycle is
isRelatedToEx→unionOrIntersectionRelatedTo→eachTypeRelatedToType→isRelatedToExwith no depth check, consuming the full 1GB goroutine stack.Changes
overallDepthcounter toRelater, checked in theskipCachingbranch with a limit of 100 (matchingrecursiveTypeRelatedTo). Setsoverflow = trueon exceeding, producing proper "Excessive stack depth" diagnostics instead of crashing.depthcounter toInferenceStatewith a limit of 200 ininferFromTypesas defense-in-depth against unboundedinferFromTypes→inferToMultipleTypes→inferFromTypesrecursion (not protected byinvokeOnce).recursiveTupleTypeWithSpreadOverflow.ts.📱 Kick off Copilot coding agent tasks wherever you are with GitHub Mobile, available on iOS and Android.