Skip to content

security: cap DOM-walk recursion depth to stop a script-built tree from crashing the process - #265

Merged
tannevaled merged 1 commit into
mainfrom
fix-tree-depth-stack-overflow
Oct 9, 2026
Merged

tannevaled merged 1 commit into
mainfrom
fix-tree-depth-stack-overflow

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Summary

  • golang.org/x/net/html already refuses to parse markup nested past 512 levels, but that guard lives in the parser alone — a page's own <script> can build an arbitrarily deep tree at runtime (createElement + appendChild in a loop), since dom.AppendChild has no depth check of its own.
  • Four unbounded recursive DOM walks (css.StylesheetLinks, styleElementText, css.Cascade's element walk, and layout's place/contents) then hit an unrecoverable fatal error: stack overflow (not a panic — recover() cannot catch it) somewhere between 150,000 and 200,000 levels, confirmed with a synthetic tree built directly in Go. This kills the whole embedding process, not just the one render.
  • Each walk now caps its recursion depth at 512 (reusing the HTML parser's own empirically-confirmed bound) and simply stops descending past it, the same "cap and move on" shape this codebase already uses for @import cycles (maxImportDepth) and var() cycles (maxVarDepth).
  • Paint needs no separate cap: it only walks the already-built layout.Box tree, which layout's own cap keeps shallow.

Test plan

  • go build ./...
  • go test ./... (all packages pass)
  • New tests: TestCascadeDoesNotCrashOnAPathologicallyDeepTree, TestStylesheetLinksDoesNotCrashOnAPathologicallyDeepTree (css), TestLayoutDoesNotCrashOnAPathologicallyDeepTree (layout) — each hand-builds a 10,000-level chain and confirms the walk returns instead of crashing
  • Confirmed the fix end-to-end against a 500,000-deep synthetic tree (cascade → layout → paint completes cleanly where it previously crashed)
  • layout package coverage restored to 100%; css package's pre-existing 99.5% floor is unaffected (confirmed identical before/after via git stash)

🤖 Generated with Claude Code

…om crashing the process

golang.org/x/net/html already refuses to parse markup nested past 512
levels, but that guard lives in the parser alone: a page's own script
can still build an arbitrarily deep tree at runtime (createElement +
appendChild in a loop), since dom.AppendChild has no depth check of
its own. Four unbounded recursive DOM walks (css.StylesheetLinks,
styleElementText, css.Cascade's element walk, and layout's place/
contents) then hit an unrecoverable "fatal error: stack overflow"
(not a panic -- recover() cannot catch it) between roughly 150,000
and 200,000 levels, killing the whole embedding process rather than
just the one render.

Each walk now caps its recursion at 512 (matching the parser's own
bound) and stops descending past it instead of crashing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit b3aee1e into main Oct 9, 2026
7 checks passed
@tannevaled
tannevaled deleted the fix-tree-depth-stack-overflow branch October 9, 2026 12:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant