[GH-ISSUE #3839] [Bug] Getter-result expansion grows combinatorially and crashes semantic analysis of a shipped bundle #1480

Closed
opened 2026-10-16 21:06:00 +00:00 by bsakaguchi · 7 comments
Owner

Originally created by @kaoru0822-kitauji on GitHub (Oct 15, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/3839

Summary: semantic analysis of one shipped bundle fails with RangeError: Maximum call stack size exceeded on current main. The exception is not deep recursion: it comes from possibleResults.push(...getterResultReferences(…)) in semanticClassPropertyReferences (javascriptSemanticObjectReferences.ts), when the spread array exceeds V8's argument limit. Removing the spread does not fix it. The same file then runs for 340 s and exhausts an 8 GB heap, because getter-result expansion grows combinatorially.

Artifact (shipped, not attached): Claude for macOS 2.37337.1, app.asar → .vite/build/index.chunk-e5zmsebk.js, 1,296,396 B, SHA-256 a49783e6f17fafcde3ee7fe072a7864b11cadbea918bd2d9a15f95b9311cae7f. It contains a bundled undici-style Request class whose getters return fields of a symbol-keyed state object:

get method(){return P.brandCheck(this,U),this[M].method}
get redirect(){return P.brandCheck(this,U),this[M].redirect}
get body(){return P.brandCheck(this,U),this[M].body?this[M].body.stream:null}
// … many more

Observed (analyzeParsedJavaScriptSemantics, Node 26.5, macOS arm64):

  • main (4135c91): throws RangeError: Maximum call stack size exceeded after ~2.3 s. The trace runs collectSemanticMemberMutationsSteps → markEscaped → escapedReferenceLeaves → expandedValues → referencedValues → semanticClassPropertyReferences ⇄ getterResultReferences, only about 6 frames deep.
  • I instrumented getterResultReferences: individual calls return 2,282 references over only 7 distinct nodes, with paths that are permutations of getter names, e.g. ["reloadNavigation","body","stream","redirect","referrerPolicy","destination","method"] and ["mode","body","stream","credentials","referrerPolicy","destination","method"], each repeated many times.
  • With the spread replaced by a loop, the file did not finish: 340 s, then a V8 heap OOM at ~8 GB.

Likely mechanism: this[M] reads with a computed key that is unknown statically, so it can match any accessor of the class. Each matched getter's result again reads this[M].<field>, and the expansion enumerates orderings of getter names. The per-getter guard in getterResultReferences (previousLength <= path.length) stops revisiting one getter with a non-shrinking path, but it doesn't bound the number of distinct orderings across many getters.

Scope notes:

  • The other 420 Claude.app files, plus Discord, Notion, Signal, Tabby (2,183 files, including unbundled undici 8.10.2) and VS Code out/, analyze normally.
  • A rolldown bundle of undici 8.10.2's index-fetch.js (412 KB) also analyzes fine (540 ms), so the trigger needs more of this chunk's surrounding code. I don't have a public minimal reproduction yet.
  • In the CLI the semantic phase runs in the JavaScript worker, so this file loses its semantic facts rather than crashing the process.

Expected: getter-result expansion should be bounded (for example by a per-query reference budget, or by collapsing unknown-key receiver reads to one conservative effect), with exhaustion reported as unknown coverage rather than an exception or unbounded growth. Two separate safety fixes also seem worthwhile: push(...array) on unbounded reference lists in this module should become a loop, and an unknown computed key on this should probably not fan out to every accessor.

Originally created by @kaoru0822-kitauji on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3839 **Summary:** semantic analysis of one shipped bundle fails with `RangeError: Maximum call stack size exceeded` on current `main`. The exception is not deep recursion: it comes from `possibleResults.push(...getterResultReferences(…))` in `semanticClassPropertyReferences` (`javascriptSemanticObjectReferences.ts`), when the spread array exceeds V8's argument limit. Removing the spread does not fix it. The same file then runs for **340 s and exhausts an 8 GB heap**, because getter-result expansion grows combinatorially. **Artifact (shipped, not attached):** Claude for macOS 2.37337.1, `app.asar` → `.vite/build/index.chunk-e5zmsebk.js`, 1,296,396 B, SHA-256 `a49783e6f17fafcde3ee7fe072a7864b11cadbea918bd2d9a15f95b9311cae7f`. It contains a bundled undici-style `Request` class whose getters return fields of a symbol-keyed state object: ```js get method(){return P.brandCheck(this,U),this[M].method} get redirect(){return P.brandCheck(this,U),this[M].redirect} get body(){return P.brandCheck(this,U),this[M].body?this[M].body.stream:null} // … many more ``` **Observed** (`analyzeParsedJavaScriptSemantics`, Node 26.5, macOS arm64): - `main` (4135c91): throws `RangeError: Maximum call stack size exceeded` after ~2.3 s. The trace runs `collectSemanticMemberMutationsSteps` → `markEscaped` → `escapedReferenceLeaves` → `expandedValues` → `referencedValues` → `semanticClassPropertyReferences` ⇄ `getterResultReferences`, only about 6 frames deep. - I instrumented `getterResultReferences`: individual calls return **2,282** references over only **7** distinct nodes, with paths that are permutations of getter names, e.g. `["reloadNavigation","body","stream","redirect","referrerPolicy","destination","method"]` and `["mode","body","stream","credentials","referrerPolicy","destination","method"]`, each repeated many times. - With the spread replaced by a loop, the file did not finish: 340 s, then a V8 heap OOM at ~8 GB. **Likely mechanism:** `this[M]` reads with a computed key that is unknown statically, so it can match any accessor of the class. Each matched getter's result again reads `this[M].<field>`, and the expansion enumerates orderings of getter names. The per-getter guard in `getterResultReferences` (`previousLength <= path.length`) stops revisiting one getter with a non-shrinking path, but it doesn't bound the number of distinct orderings across many getters. **Scope notes:** - The other 420 Claude.app files, plus Discord, Notion, Signal, Tabby (2,183 files, including unbundled undici 8.10.2) and VS Code `out/`, analyze normally. - A rolldown bundle of undici 8.10.2's `index-fetch.js` (412 KB) also analyzes fine (540 ms), so the trigger needs more of this chunk's surrounding code. I don't have a public minimal reproduction yet. - In the CLI the semantic phase runs in the JavaScript worker, so this file loses its semantic facts rather than crashing the process. **Expected:** getter-result expansion should be bounded (for example by a per-query reference budget, or by collapsing unknown-key receiver reads to one conservative effect), with exhaustion reported as unknown coverage rather than an exception or unbounded growth. Two separate safety fixes also seem worthwhile: `push(...array)` on unbounded reference lists in this module should become a loop, and an unknown computed key on `this` should probably not fan out to every accessor.
Author
Owner

@XinwangTao commented on GitHub (Oct 15, 2026):

Thanks for the detailed write-up — the mechanism makes sense: an unknown computed key like this[M] matching every accessor turns getter-chain exploration into a permutation problem, and the previousLength <= path.length guard only stops revisiting one getter, not the combinatorial blowup across distinct orderings. I agree bounding the expansion is the right direction; a per-query reference budget plus memoizing already-expanded (receiver, path) pairs would keep the worst case predictable without losing current dedup behaviour. Would be curious whether #3846/#3848 take the budget approach or the conservative-collapse for unknown-key reads.

<!-- gh-comment-id:6184793162 --> @XinwangTao commented on GitHub (Oct 15, 2026): Thanks for the detailed write-up — the mechanism makes sense: an unknown computed key like this[M] matching every accessor turns getter-chain exploration into a permutation problem, and the previousLength <= path.length guard only stops revisiting one getter, not the combinatorial blowup across distinct orderings. I agree bounding the expansion is the right direction; a per-query reference budget plus memoizing already-expanded (receiver, path) pairs would keep the worst case predictable without losing current dedup behaviour. Would be curious whether #3846/#3848 take the budget approach or the conservative-collapse for unknown-key reads.
Author
Owner

@hannnnnnnny commented on GitHub (Oct 15, 2026):

2. Thanks for the detailed write-up — the mechanism makes sense: an unknown computed key like this[M] matching every accessor turns getter-chain exploration into a permutation problem, and the previousLength <= path.length guard only stops revisiting one getter, not the combinatorial blowup across distinct orderings. I agree bounding the expansion is the right direction; a per-query reference budget plus memoizing already-expanded (receiver, path) pairs would keep the worst case predictable without losing current dedup behaviour. Would be curious whether #3846/#3848 take the budget approach or the conservative-collapse for unknown-key reads.

Thanks! #3846 was superseded by #3848. #3848 currently takes the budget approach only (256 getter visits + retained results per property-reference query, degrading to explicit unknown coverage), without the (receiver, path) memoization or the unknown-key collapse.

Testing showed the budget alone is too coarse: a class with ~130 plain getters plus one unknown-key read now trips it, and the fallback marks the whole file's values and module links unknown. I'm revising #3848 along the lines you suggested: memoize expansion per query by (getter/receiver, remaining path) so permutations collapse, keep the budget only as a safety net, and degrade just the affected query instead of the whole file.

<!-- gh-comment-id:6185098121 --> @hannnnnnnny commented on GitHub (Oct 15, 2026): > 2\. Thanks for the detailed write-up — the mechanism makes sense: an unknown computed key like this[M] matching every accessor turns getter-chain exploration into a permutation problem, and the previousLength <= path.length guard only stops revisiting one getter, not the combinatorial blowup across distinct orderings. I agree bounding the expansion is the right direction; a per-query reference budget plus memoizing already-expanded (receiver, path) pairs would keep the worst case predictable without losing current dedup behaviour. Would be curious whether [#3846](https://github.com/morluto/rea/pull/3846)/[#3848](https://github.com/morluto/rea/pull/3848) take the budget approach or the conservative-collapse for unknown-key reads. Thanks! #3846 was superseded by #3848. #3848 currently takes the budget approach only (256 getter visits + retained results per property-reference query, degrading to explicit unknown coverage), without the (receiver, path) memoization or the unknown-key collapse. Testing showed the budget alone is too coarse: a class with ~130 plain getters plus one unknown-key read now trips it, and the fallback marks the whole file's values and module links unknown. I'm revising #3848 along the lines you suggested: memoize expansion per query by (getter/receiver, remaining path) so permutations collapse, keep the budget only as a safety net, and degrade just the affected query instead of the whole file.
Author
Owner

@hannnnnnnny commented on GitHub (Oct 15, 2026):

Update: #3848 has been revised along those lines and is ready for review.

  • Each property-reference query now memoizes getter expansion by (getter, receiver, remaining path, escape mode), so permutations collapse instead of being re-enumerated. The 512-state budget is only a safety net.
  • On a growing cycle or limit hit, only the affected query retries with escaped getter results; direct field writes, unrelated values and module links are preserved, and coverage reports getter-expansion.
  • Unknown-key reads still fan out to every accessor (no conservative collapse yet); that is left as a follow-up.

The original macOS bundle is still unavailable, so #3848 uses Refs #3839 rather than closing this issue. The same RangeError on a shipped Claude chunk was also observed independently in the corpus comparisons of #3857, #3860 and #3897.

<!-- gh-comment-id:6186514326 --> @hannnnnnnny commented on GitHub (Oct 15, 2026): Update: #3848 has been revised along those lines and is ready for review. - Each property-reference query now memoizes getter expansion by (getter, receiver, remaining path, escape mode), so permutations collapse instead of being re-enumerated. The 512-state budget is only a safety net. - On a growing cycle or limit hit, only the affected query retries with escaped getter results; direct field writes, unrelated values and module links are preserved, and coverage reports `getter-expansion`. - Unknown-key reads still fan out to every accessor (no conservative collapse yet); that is left as a follow-up. The original macOS bundle is still unavailable, so #3848 uses `Refs #3839` rather than closing this issue. The same RangeError on a shipped Claude chunk was also observed independently in the corpus comparisons of #3857, #3860 and #3897.
Author
Owner

@XinwangTao commented on GitHub (Oct 15, 2026):

The revised #3848 approach — memoizing expansion by (getter, receiver, remaining path, escape mode) with a 512-state budget as a safety net — addresses exactly the weakness the budget-only version had: a class with ~130 plain getters tripping the coarse budget and marking the whole file unknown. Scoping the degradation to the affected query instead of the whole file is the right call. Will take a look at the PR.

<!-- gh-comment-id:6188116976 --> @XinwangTao commented on GitHub (Oct 15, 2026): The revised #3848 approach — memoizing expansion by (getter, receiver, remaining path, escape mode) with a 512-state budget as a safety net — addresses exactly the weakness the budget-only version had: a class with ~130 plain getters tripping the coarse budget and marking the whole file unknown. Scoping the degradation to the affected query instead of the whole file is the right call. Will take a look at the PR.
Author
Owner

@XinwangTao commented on GitHub (Oct 15, 2026):

The revised #3848 approach — memoizing expansion by (getter, receiver, remaining path, escape mode) with a 512-state budget as a safety net — addresses exactly the weakness the budget-only version had: a class with ~130 plain getters tripping the coarse budget and marking the whole file unknown. Scoping the degradation to the affected query instead of the whole file is the right call. Will take a look at the PR.

<!-- gh-comment-id:6188171182 --> @XinwangTao commented on GitHub (Oct 15, 2026): The revised #3848 approach — memoizing expansion by (getter, receiver, remaining path, escape mode) with a 512-state budget as a safety net — addresses exactly the weakness the budget-only version had: a class with ~130 plain getters tripping the coarse budget and marking the whole file unknown. Scoping the degradation to the affected query instead of the whole file is the right call. Will take a look at the PR.
Author
Owner

@CassieFox23 commented on GitHub (Oct 18, 2026):

Checked on Linux: this is fixed on main by 880b3a510. The commit's message says "Refs #3839", which is why GitHub left this issue open.

  • The five test files from the commit, such as src/domain/javascript/javascriptSemanticAnalysis.getter_expansion.test.ts, pass on main 36d156ff0 on Node 22.23.1, 24.18.0 and 26.11.1 (92 tests).
  • On the commit's parent, 8 of 89 fail, RangeError: Maximum call stack size exceeded (the issue's own error).

Thanks for the clear report. It made this easy to check, kaoru0822-kitauji.

If nothing else is open here, this can be closed.

🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊

<!-- gh-comment-id:6254890835 --> @CassieFox23 commented on GitHub (Oct 18, 2026): Checked on Linux: this is fixed on main by 880b3a510. The commit's message says "Refs #3839", which is why GitHub left this issue open. - The five test files from the commit, such as `src/domain/javascript/javascriptSemanticAnalysis.getter_expansion.test.ts`, pass on main 36d156ff0 on Node 22.23.1, 24.18.0 and 26.11.1 (92 tests). - On the commit's parent, 8 of 89 fail, `RangeError: Maximum call stack size exceeded` (the issue's own error). Thanks for the clear report. It made this easy to check, kaoru0822-kitauji. If nothing else is open here, this can be closed. 🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊
Author
Owner

@morluto commented on GitHub (Oct 21, 2026):

Closing as fixed on main by github.com/morluto/rea@880b3a510b, retained at 67a6477f01.

Getter expansion is owned by a bounded query worklist with memoization and conservative per-query widening, replacing the combinatorial expansion. Main includes the targeted getter regressions; the latest issue comment records 92 tests passing on Node 22/24/26 and eight failures on the fix's parent. No fresh shipped-bundle benchmark was run in this audit. PR #3848 remains open for its distinct current regression-test delta.

This closure follows current-main producer and regression source inspection and the existing issue evidence. Tests and runtime reproductions were not rerun in this audit; no additional platform/provider coverage is implied.

<!-- gh-comment-id:6324294016 --> @morluto commented on GitHub (Oct 21, 2026): Closing as fixed on main by https://github.com/morluto/rea/commit/880b3a510bd8bbea2a7d4933549d8070b0409afe, retained at 67a6477f011ae358fc333e5a6e26c86d932654ed. Getter expansion is owned by a bounded query worklist with memoization and conservative per-query widening, replacing the combinatorial expansion. Main includes the targeted getter regressions; the latest issue comment records 92 tests passing on Node 22/24/26 and eight failures on the fix's parent. No fresh shipped-bundle benchmark was run in this audit. PR #3848 remains open for its distinct current regression-test delta. This closure follows current-main producer and regression source inspection and the existing issue evidence. Tests and runtime reproductions were not rerun in this audit; no additional platform/provider coverage is implied.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#1480
No description provided.