Skip to content

[BUG] A deeply nested plan overflows the stack before MaxParseDepth can refuse it, ending the process #589

Closed

Description

@erikdarlingdata

Claude posting for Erik Darling

Component

All: the shared parser in PlanViewer.Core (Services/ShowPlanParser.cs)

Performance Studio Version

dev @ 5b7a6c7. Measured against main @ 7a16042, which has the same parser structure.

Operating System

Any. The numbers below are for 1 MB and 1.5 MB thread stacks; 1 MB is the Windows default for a main/UI thread.

Describe the Bug

A legitimately deep plan crashes the process with a stack overflow before MaxParseDepth (1,000) can refuse it. A stack overflow can't be caught in .NET, so the process ends; it doesn't show an error.

Measured in a crash-safe child process against main @ 7a16042, running parse + analyze on the calling thread:

  • 1 MB stack: the process dies at a plan depth of about 450–480 nested operators.
  • 1.5 MB stack: it dies at about 650–750.

MaxParseDepth is 1,000 (ShowPlanParser.cs:19), so the guard is unreachable on a normal caller thread. dev @ 5b7a6c7 has the same shape: the recursive walk runs on whatever thread calls Parse, with no dedicated larger-stack thread.

Plans with hundreds of nested operators are uncommon but real (large reporting queries, deeply nested views). A crafted plan can also get there under the size cap.

PerformanceMonitor had the same gap, at a shallower depth (~80 levels on 1 MB), and fixed it in erikdarlingdata/PerformanceMonitor#4551 (issue erikdarlingdata/PerformanceMonitor#4512):

  • Parse walks on a dedicated thread with a 32 MB stack, joined synchronously, so the public signature is unchanged. The 1,000-level guard now fires, and 1,001 levels gives a clean parse error.
  • An explicit-stack loop replaced one recursive iterator over non-operator children, which even 32 MB didn't cover for crafted input.
  • A catch-all on that thread, so nothing can go unhandled there.
  • Pins: parse→analyze→score→layout at depth 150 and 999 on real 1 MB and 1.5 MB threads, and 1,001 refused.

Steps to Reproduce

  1. Generate a showplan whose RelOp nesting is ~500 levels deep (for example, a synthetic plan built programmatically, or a very deep nested-loop / nested-view query plan).
  2. Open or analyze it in Performance Studio (the desktop app or planview analyze).

Expected Behavior

Plans up to MaxParseDepth parse, and deeper ones are refused with a clear parse error. The app keeps running.

Actual Behavior

The process terminates with a stack overflow at around 450–480 levels on a 1 MB stack.

Plan File

A synthetic nested-RelOp plan reproduces it. The generator PerformanceMonitor used for its pins produces one at any depth.

Activity

  1. added 3 commits that reference this issue on Sep 28, 2026
  2. erikdarlingdata commented on Sep 28, 2026

    @erikdarlingdata
    OwnerAuthor

    Fixed by #591, merged to dev. Ships in the next release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions