dotnet / dotnet/aspnetcore

Static SSR: stack overflow — StaticHtmlRenderer's HTML walk cycles through a disposed SectionOutletContentRenderer when a section registration races the walk

Open
#69,035 3 comments 0 reactions 1 assignee Claimed by @PreethikaSelvam View on GitHub
area-blazor Needs: Author Feedback Needs: Repro
Dominant language
C#
Stars
38.4k
Forks
10.9k
Avg merge
2d 10h
Merged PRs (30d)
281

Description

### Is there an existing issue for this?

- [x] I have searched the existing issues

### Describe the bug

Intermittently (a few times per day across our CI runs; never reproduced locally on Windows), a static-SSR page render dies with a process-killing stack overflow ~15,000 frames deep:

```
Stack overflow.
at ...StaticHtmlRenderer.RenderElement(...)
at ...StaticHtmlRenderer.RenderCore(...) [HtmlWriting.cs @ 90]
at ...EndpointHtmlRenderer.RenderChildComponent(...) [EndpointHtmlRenderer.Streaming.cs @ 265]
(repeating unit, thousands of times)
...
at ...EndpointHtmlRenderer.WriteComponentHtml(Int32, System.IO.TextWriter)
at ...RazorComponentEndpointInvoker+d__5.MoveNext()
... (WaitForResultReady / WaitForNonStreamingPendingTasks continuations)
```

We enabled `DOTNET_DbgEnableMiniDump` in CI and analysed two independent core dumps with `dotnet-dump`. Both show the same signature:

- The crashing renderer holds a normal-sized tree (~25 `EndpointComponentState`s), **except exactly one `SectionOutlet+SectionOutletContentRenderer` whose `_componentWasDisposed` is `true`** while it is still referenced from a live ancestor's `CurrentRenderTree`.
- The frame-walk arguments show the recursion is a **cycle over the same handful of componentIds** (e.g. `22 → 21 → 12 → 0 → 22 …`: two `CascadingValue`s → `AuthorizeRouteViewCore` → root → back), not 15k real children — the walk re-enters an ancestor via the stale frame and never terminates.
- Dump 1's tree: an ordinary static page (`Router → AuthorizeRouteView → LayoutView → layout → page`) with `PageTitle`s in both the layout and the page. Dump 2's tree: **no page markup at all** — root component, `HeadOutlet` (both its internal `SectionOutlet`s and their content renderers, one of them the disposed one), router/layout chrome, and an `SSRRenderModeBoundary` (the page is `InteractiveServer` with `prerender: false`), with a single `PageTitle` in the layout.

So the trigger appears to be: a **section content registration/replacement (here: `HeadOutlet`/`PageTitle`) lands while `WriteComponentHtml`'s synchronous walk is in progress**, the outlet's old content renderer is disposed and replaced, an ancestor's render-tree frame still points at the disposed one, and `RenderChildComponent` walks its stale `CurrentRenderTree` back up into its own ancestors — an infinite cycle ending in a process-killing `StackOverflowException`.

Reducing the number of `PageTitle` swaps app-side (hoisting one `PageTitle` per page above all data branches) reduced the frequency but did not eliminate it — dump 2 shows a single layout-level `PageTitle` still racing the walk.

### Expected Behavior

The HTML walk either runs against a stable snapshot of the section outlets, or a disposed content renderer is never reachable from a frame the walk visits — never an unbounded cycle that takes the process down with SIGABRT.

### Steps To Reproduce

No minimal repro yet — timing-dependent (2-core CI runners, `WebApplicationFactory` integration tests requesting static-SSR pages; several distinct pages have been the victim). Happy to share the two core dumps' `dotnet-dump` output (heap listings of the component states incl. the disposed renderer, and the cycling componentId sequence from `clrstack -a`) or run instrumented builds.

### .NET Version

10.0.11 (ASP.NET Core 10.0.11), Linux x64 (ubuntu-24.04 GitHub runners)

### Anything else?

Blazor Web App; static SSR by default with InteractiveServer islands; `HeadOutlet` in the document root; `PageTitle` per page; enhanced navigation enabled.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.