Skip to content

parser: make stack-overflow failure sticky across save-point backtracking#297

Open
robobun wants to merge 2 commits into
mainfrom
farm/9b0a76df/parser-sticky-stack-overflow
Open

parser: make stack-overflow failure sticky across save-point backtracking#297
robobun wants to merge 2 commits into
mainfrom
farm/9b0a76df/parser-sticky-stack-overflow

Conversation

@robobun

@robobun robobun commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Reproduction

python3 -c "print('const y = ' + '{a:'*3000 + '1' + '}'*3000 + ';')" > nest3000.js
jsc nest3000.js

Before: hangs forever at 99% CPU with monotonic RSS growth (never terminates).
After: RangeError: Maximum call stack size exceeded. in <50ms, including at depth 100000.

Cause

When failIfStackOverflow() trips inside parseAssignmentExpression for a deeply nested {a:{a:...}}, the error propagates back to each enclosing parseAssignmentExpression frame, whose maybeAssignmentPattern branch then calls swapSavePointForError (clearing m_errorMessage) and retries the same input via tryParseDestructuringPatternExpression. That path reaches parseAssignmentElement, which on failure of its parseDestructuringPattern attempt calls restoreSavePoint (clearing the error again) and retries via parseMemberExpression, which re-enters parseAssignmentExpression. Two alternate-production retries at each nesting level multiply to exponential work; the observable result is an apparent hang and unbounded StringPrintStream / arena allocation.

Fix

m_hasStackOverflow is already set by failWithStackOverflow() and is not touched by restoreSavePoint. Checking it in failIfStackOverflow() makes the first overflow short-circuit every subsequent recursion entry (parseAssignmentExpression, parsePrimaryExpression, parseDestructuringPattern, ...) so the failure propagates in O(depth).

Verification

jsc file depth=2600  ok
jsc file depth=3000  Exception: RangeError: Maximum call stack size exceeded.
jsc file depth=100000 Exception: RangeError: Maximum call stack size exceeded.

Destructuring, arrow functions, assignment patterns, array destructuring, and shallow nested objects all continue to parse normally.

…king

When the recursion limit fires in parseAssignmentExpression for a deeply
nested object or array literal, each enclosing level retries the input as a
destructuring pattern (parseAssignmentExpression) and again as a member
expression (parseAssignmentElement). Both retry paths call restoreSavePoint,
which clears m_errorMessage, so the stack check has to fire again at the next
recursion entry. With two retry branches at every level the work becomes
exponential in nesting depth, and the parser appears to hang with unbounded
memory growth instead of surfacing the RangeError.

m_hasStackOverflow already survives restoreSavePoint; this change makes
failIfStackOverflow() consult it so the first overflow short-circuits every
subsequent recursion entry and the failure propagates in O(depth).
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 20 seconds

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 16b2149f-7dfa-4d51-8942-cd27912464f8

📥 Commits

Reviewing files that changed from the base of the PR and between 8b31a3b and 3946a08.

📒 Files selected for processing (2)
  • .github/workflows/build-reusable.yml
  • Dockerfile

Walkthrough

failIfStackOverflow() now treats m_hasStackOverflow as a sticky condition, preserving stack-overflow handling across subsequent parser checks and retries.

Changes

Parser stack-overflow handling

Layer / File(s) Summary
Persist overflow detection
Source/JavaScriptCore/parser/Parser.cpp
failIfStackOverflow() triggers when stack overflow was previously recorded or recursion can no longer continue, rather than checking recursion availability alone.
🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the bug, cause, fix, and verification, but it omits template-required items like the bug link, reviewed-by line, and file list. Add the Bugzilla link, Reviewed by line, bug title, and changed file/function list to match the repository's pull request template.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main parser change to sticky stack-overflow handling.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — minimal, well-reasoned one-line fix that turns a parser hang into a prompt RangeError.

What was reviewed:

  • Confirmed failWithStackOverflow() sets m_hasStackOverflow and nothing ever clears it; restoreSavePoint/swapSavePointForError only reset m_errorMessage, so the sticky check is sound.
  • m_hasStackOverflow is initialized to false per-Parser-instance (Parser.cpp:146), so no cross-parse leakage.
  • Short-circuit ordering keeps [[unlikely]] accurate and avoids the isSafeToRecurse() call once tripped.
Extended reasoning...

Overview

Single-line change to the failIfStackOverflow() macro in Source/JavaScriptCore/parser/Parser.cpp, adding m_hasStackOverflow || ahead of the existing !canRecurse() check. A three-line comment explains the rationale. No other files or logic touched.

Security risks

None introduced. This is effectively a DoS-class fix: previously, deeply nested object literals ({a:{a:...}} ~3000 deep) triggered exponential backtracking after the recursion limit was hit, hanging the process with unbounded RSS growth. The change makes the first overflow terminal so failure propagates in O(depth). It strictly reduces attack surface.

Level of scrutiny

Low-to-moderate. The diff is one boolean disjunct in a macro. I verified the invariants it relies on:

  • failWithStackOverflow() (Parser.cpp:48) is the only writer of m_hasStackOverflow and only sets it true.
  • restoreSavePoint (Parser.h:2082) and swapSavePointForError (Parser.h:2043) clear m_errorMessage but never touch m_hasStackOverflow, which is exactly the gap this patch closes.
  • The Parser constructor initializes m_hasStackOverflow = false (Parser.cpp:146) and Parser instances are per-parse, so stickiness cannot leak across unrelated parses.
  • Once real stack overflow is hit, there is no legitimate alternate production that could succeed by recursing again, so making it sticky cannot cause a false rejection of valid input.

Other factors

The PR description includes a clear reproduction, root-cause trace through parseAssignmentExpressiontryParseDestructuringPatternExpressionparseAssignmentElementparseMemberExpression, and before/after verification at depths 2600/3000/100000 plus regression checks on normal destructuring/arrow/array patterns. The bug hunting system found no issues. The change is mechanical enough and its supporting invariants easy enough to verify that human review would not add signal.

…atrix

ports.ubuntu.com has been unreachable for hours, and a single arm64 apt
failure was cancelling all 12 Linux variants. Retry apt with a
Launchpad-registered mirror on failure, and let the remaining matrix
entries finish when one variant dies. Mirrors the same hardening already
on the dylan/llvm-22 branch.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't find any issues, but the parser macro change — while minimal — alters behavior on a core JSC parse path, so it's worth a human sign-off.

What was reviewed:

  • Verified m_hasStackOverflow is only set by failWithStackOverflow(), initialized to false per-Parser, and never cleared by restoreSavePoint/swapSavePointForError — so the sticky check can only short-circuit an already-doomed parse, never reject a previously-accepted one.
  • Dockerfile retry loop: confirmed the trailing dpkg -l guard fails the build if all 3 attempts exhaust without installing.
  • fail-fast: false additions match the existing windows-cross job pattern.
Extended reasoning...

Overview

Three independent changes bundled together:

  1. Parser.cpp (1 line + comment): failIfStackOverflow() now also checks the sticky m_hasStackOverflow flag, so once the recursion limit trips, every subsequent failIfStackOverflow() call fails immediately even after save-point backtracking has cleared m_errorMessage. Fixes an exponential-retry hang on deeply nested object literals.
  2. .github/workflows/build-reusable.yml: adds fail-fast: false to five matrix jobs (linux, linux-musl, macos-cross, freebsd, linux-android) so one variant's failure doesn't cancel siblings.
  3. Dockerfile: replaces the two-mirror apt-get update fallback with a 3-attempt retry loop over update+install combined, swapping to fallback mirrors (archive.ubuntu.com for amd64, ocf.berkeley.edu for arm64 ports) between attempts, plus Acquire::Retries "5" and a final dpkg -l sanity check.

Security risks

None identified. The parser change only adds a fast-fail condition on a flag that is exclusively set by the existing overflow path; it cannot accept input that was previously rejected. The Dockerfile change adds a third-party mirror (mirrors.ocf.berkeley.edu) as an arm64 fallback — a Launchpad-registered mirror, and the packages are signed, so apt's signature check still gates what gets installed.

Level of scrutiny

The CI and Dockerfile changes are low-risk infrastructure resilience. The parser change is tiny and I traced its correctness (m_hasStackOverflow is per-instance, init-false at Parser.cpp:146, set only at Parser.cpp:48, never touched by restoreSavePoint/internalRestoreState in Parser.h:2082-2086), but it lives in JavaScriptCore's hot parse path where any behavioral edge deserves a maintainer's eyes rather than a bot approval.

Other factors

The PR description is thorough with a reproduction and root-cause trace. The two commits already appear on main in the local checkout, suggesting this may already be merged. No prior human or claude[bot] review on the thread; coderabbit was rate-limited. No tests added, but the fix is a guard-condition tightening that's hard to regression-test without a stack-depth-dependent harness.

@github-actions

Copy link
Copy Markdown

Preview Builds

Commit Release Date
3946a08b autobuild-preview-pr-297-3946a08b 2026-07-16 09:33:25 UTC

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